5c0f209a30e98bebe7dab028e6a17453cabfd54e
[wolnelektury.git] / apps / catalogue / views.py
1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 #
5 import tempfile
6 import zipfile
7 import tarfile
8 import sys
9 import pprint
10 import traceback
11 import re
12 import itertools
13 from datetime import datetime
14
15 from django.conf import settings
16 from django.template import RequestContext
17 from django.shortcuts import render_to_response, get_object_or_404
18 from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect
19 from django.core.urlresolvers import reverse
20 from django.db.models import Count, Sum, Q
21 from django.contrib.auth.decorators import login_required, user_passes_test
22 from django.utils.datastructures import SortedDict
23 from django.views.decorators.http import require_POST
24 from django.contrib import auth
25 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
26 from django.utils import simplejson
27 from django.utils.functional import Promise
28 from django.utils.encoding import force_unicode
29 from django.utils.http import urlquote_plus
30 from django.views.decorators import cache
31 from django.utils import translation
32 from django.utils.translation import ugettext as _
33 from django.views.generic.list_detail import object_list
34
35 from catalogue import models
36 from catalogue import forms
37 from catalogue.utils import split_tags
38 from newtagging import views as newtagging_views
39 from pdcounter import models as pdcounter_models
40 from pdcounter import views as pdcounter_views
41 from suggest.forms import PublishingSuggestForm
42 from slughifi import slughifi
43
44
45 staff_required = user_passes_test(lambda user: user.is_staff)
46
47
48 class LazyEncoder(simplejson.JSONEncoder):
49     def default(self, obj):
50         if isinstance(obj, Promise):
51             return force_unicode(obj)
52         return obj
53
54 # shortcut for JSON reponses
55 class JSONResponse(HttpResponse):
56     def __init__(self, data={}, callback=None, **kwargs):
57         # get rid of mimetype
58         kwargs.pop('mimetype', None)
59         data = simplejson.dumps(data)
60         if callback:
61             data = callback + "(" + data + ");" 
62         super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs)
63
64
65 def main_page(request):
66     if request.user.is_authenticated():
67         shelves = models.Tag.objects.filter(category='set', user=request.user)
68         new_set_form = forms.NewSetForm()
69
70     tags = models.Tag.objects.exclude(category__in=('set', 'book'))
71     for tag in tags:
72         tag.count = tag.get_count()
73     categories = split_tags(tags)
74     fragment_tags = categories.get('theme', [])
75
76     form = forms.SearchForm()
77     return render_to_response('catalogue/main_page.html', locals(),
78         context_instance=RequestContext(request))
79
80
81 def book_list(request, filter=None, template_name='catalogue/book_list.html'):
82     """ generates a listing of all books, optionally filtered with a test function """
83
84     form = forms.SearchForm()
85
86     books_by_parent = {}
87     books = models.Book.objects.all().order_by('parent_number', 'sort_key').only('title', 'parent', 'slug')
88     if filter:
89         books = books.filter(filter).distinct()
90         book_ids = set((book.pk for book in books))
91         for book in books:
92             parent = book.parent_id
93             if parent not in book_ids:
94                 parent = None
95             books_by_parent.setdefault(parent, []).append(book)
96     else:
97         for book in books:
98             books_by_parent.setdefault(book.parent_id, []).append(book)
99
100     orphans = []
101     books_by_author = SortedDict()
102     books_nav = SortedDict()
103     for tag in models.Tag.objects.filter(category='author'):
104         books_by_author[tag] = []
105
106     for book in books_by_parent.get(None,()):
107         authors = list(book.tags.filter(category='author'))
108         if authors:
109             for author in authors:
110                 books_by_author[author].append(book)
111         else:
112             orphans.append(book)
113
114     for tag in books_by_author:
115         if books_by_author[tag]:
116             books_nav.setdefault(tag.sort_key[0], []).append(tag)
117
118     return render_to_response(template_name, locals(),
119         context_instance=RequestContext(request))
120
121
122 def audiobook_list(request):
123     return book_list(request, Q(media__type='mp3') | Q(media__type='ogg'),
124                      template_name='catalogue/audiobook_list.html')
125
126
127 def daisy_list(request):
128     return book_list(request, Q(media__type='daisy'),
129                      template_name='catalogue/daisy_list.html')
130
131
132 def differentiate_tags(request, tags, ambiguous_slugs):
133     beginning = '/'.join(tag.url_chunk for tag in tags)
134     unparsed = '/'.join(ambiguous_slugs[1:])
135     options = []
136     for tag in models.Tag.objects.exclude(category='book').filter(slug=ambiguous_slugs[0]):
137         options.append({
138             'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
139             'tags': [tag]
140         })
141     return render_to_response('catalogue/differentiate_tags.html',
142                 {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]},
143                 context_instance=RequestContext(request))
144
145
146 def tagged_object_list(request, tags=''):
147     try:
148         tags = models.Tag.get_tag_list(tags)
149     except models.Tag.DoesNotExist:
150         chunks = tags.split('/')
151         if len(chunks) == 2 and chunks[0] == 'autor':
152             return pdcounter_views.author_detail(request, chunks[1])
153         else:
154             raise Http404
155     except models.Tag.MultipleObjectsReturned, e:
156         return differentiate_tags(request, e.tags, e.ambiguous_slugs)
157     except models.Tag.UrlDeprecationWarning, e:
158         return HttpResponsePermanentRedirect(reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)]))
159
160     try:
161         if len(tags) > settings.MAX_TAG_LIST:
162             raise Http404
163     except AttributeError:
164         pass
165
166     if len([tag for tag in tags if tag.category == 'book']):
167         raise Http404
168
169     theme_is_set = [tag for tag in tags if tag.category == 'theme']
170     shelf_is_set = [tag for tag in tags if tag.category == 'set']
171     only_shelf = shelf_is_set and len(tags) == 1
172     only_my_shelf = only_shelf and request.user.is_authenticated() and request.user == tags[0].user
173
174     objects = only_author = None
175     categories = {}
176
177     if theme_is_set:
178         shelf_tags = [tag for tag in tags if tag.category == 'set']
179         fragment_tags = [tag for tag in tags if tag.category != 'set']
180         fragments = models.Fragment.tagged.with_all(fragment_tags)
181
182         if shelf_tags:
183             books = models.Book.tagged.with_all(shelf_tags).order_by()
184             l_tags = models.Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books])
185             fragments = models.Fragment.tagged.with_any(l_tags, fragments)
186
187         # newtagging goes crazy if we just try:
188         #related_tags = models.Tag.objects.usage_for_queryset(fragments, counts=True,
189         #                    extra={'where': ["catalogue_tag.category != 'book'"]})
190         fragment_keys = [fragment.pk for fragment in fragments]
191         if fragment_keys:
192             related_tags = models.Fragment.tags.usage(counts=True,
193                                 filters={'pk__in': fragment_keys},
194                                 extra={'where': ["catalogue_tag.category != 'book'"]})
195             related_tags = (tag for tag in related_tags if tag not in fragment_tags)
196             categories = split_tags(related_tags)
197
198             objects = fragments
199     else:
200         if shelf_is_set:
201             objects = models.Book.tagged.with_all(tags)
202         else:
203             objects = models.Book.tagged_top_level(tags)
204
205         # get related tags from `tag_counter` and `theme_counter`
206         related_counts = {}
207         tags_pks = [tag.pk for tag in tags]
208         for book in objects:
209             for tag_pk, value in itertools.chain(book.tag_counter.iteritems(), book.theme_counter.iteritems()):
210                 if tag_pk in tags_pks:
211                     continue
212                 related_counts[tag_pk] = related_counts.get(tag_pk, 0) + value
213         related_tags = models.Tag.objects.filter(pk__in=related_counts.keys())
214         related_tags = [tag for tag in related_tags if tag not in tags]
215         for tag in related_tags:
216             tag.count = related_counts[tag.pk]
217
218         categories = split_tags(related_tags)
219         del related_tags
220
221     if not objects:
222         only_author = len(tags) == 1 and tags[0].category == 'author'
223         objects = models.Book.objects.none()
224
225     return object_list(
226         request,
227         objects,
228         template_name='catalogue/tagged_object_list.html',
229         extra_context={
230             'categories': categories,
231             'only_shelf': only_shelf,
232             'only_author': only_author,
233             'only_my_shelf': only_my_shelf,
234             'formats_form': forms.DownloadFormatsForm(),
235             'tags': tags,
236         }
237     )
238
239
240 def book_fragments(request, book_slug, theme_slug):
241     book = get_object_or_404(models.Book, slug=book_slug)
242     book_tag = get_object_or_404(models.Tag, slug='l-' + book_slug, category='book')
243     theme = get_object_or_404(models.Tag, slug=theme_slug, category='theme')
244     fragments = models.Fragment.tagged.with_all([book_tag, theme])
245
246     form = forms.SearchForm()
247     return render_to_response('catalogue/book_fragments.html', locals(),
248         context_instance=RequestContext(request))
249
250
251 def book_detail(request, slug):
252     try:
253         book = models.Book.objects.get(slug=slug)
254     except models.Book.DoesNotExist:
255         return pdcounter_views.book_stub_detail(request, slug)
256
257     book_tag = book.book_tag()
258     tags = list(book.tags.filter(~Q(category='set')))
259     categories = split_tags(tags)
260     book_children = book.children.all().order_by('parent_number', 'sort_key')
261     
262     _book = book
263     parents = []
264     while _book.parent:
265         parents.append(_book.parent)
266         _book = _book.parent
267     parents = reversed(parents)
268
269     theme_counter = book.theme_counter
270     book_themes = models.Tag.objects.filter(pk__in=theme_counter.keys())
271     for tag in book_themes:
272         tag.count = theme_counter[tag.pk]
273
274     extra_info = book.get_extra_info_value()
275     hide_about = extra_info.get('about', '').startswith('http://wiki.wolnepodreczniki.pl')
276
277     projects = set()
278     for m in book.media.filter(type='mp3'):
279         # ogg files are always from the same project
280         meta = m.get_extra_info_value()
281         project = meta.get('project')
282         if not project:
283             # temporary fallback
284             project = u'CzytamySłuchając'
285
286         projects.add((project, meta.get('funded_by', '')))
287     projects = sorted(projects)
288
289     form = forms.SearchForm()
290     return render_to_response('catalogue/book_detail.html', locals(),
291         context_instance=RequestContext(request))
292
293
294 def book_text(request, slug):
295     book = get_object_or_404(models.Book, slug=slug)
296     if not book.has_html_file():
297         raise Http404
298     book_themes = {}
299     for fragment in book.fragments.all():
300         for theme in fragment.tags.filter(category='theme'):
301             book_themes.setdefault(theme, []).append(fragment)
302
303     book_themes = book_themes.items()
304     book_themes.sort(key=lambda s: s[0].sort_key)
305     return render_to_response('catalogue/book_text.html', locals(),
306         context_instance=RequestContext(request))
307
308
309 # ==========
310 # = Search =
311 # ==========
312
313 def _no_diacritics_regexp(query):
314     """ returns a regexp for searching for a query without diacritics
315
316     should be locale-aware """
317     names = {
318         u'a':u'aąĄ', u'c':u'cćĆ', u'e':u'eęĘ', u'l': u'lłŁ', u'n':u'nńŃ', u'o':u'oóÓ', u's':u'sśŚ', u'z':u'zźżŹŻ',
319         u'ą':u'ąĄ', u'ć':u'ćĆ', u'ę':u'ęĘ', u'ł': u'łŁ', u'ń':u'ńŃ', u'ó':u'óÓ', u'ś':u'śŚ', u'ź':u'źŹ', u'ż':u'żŻ'
320         }
321     def repl(m):
322         l = m.group()
323         return u"(%s)" % '|'.join(names[l])
324     return re.sub(u'[%s]' % (u''.join(names.keys())), repl, query)
325
326 def unicode_re_escape(query):
327     """ Unicode-friendly version of re.escape """
328     return re.sub('(?u)(\W)', r'\\\1', query)
329
330 def _word_starts_with(name, prefix):
331     """returns a Q object getting models having `name` contain a word
332     starting with `prefix`
333
334     We define word characters as alphanumeric and underscore, like in JS.
335
336     Works for MySQL, PostgreSQL, Oracle.
337     For SQLite, _sqlite* version is substituted for this.
338     """
339     kwargs = {}
340
341     prefix = _no_diacritics_regexp(unicode_re_escape(prefix))
342     # can't use [[:<:]] (word start),
343     # but we want both `xy` and `(xy` to catch `(xyz)`
344     kwargs['%s__iregex' % name] = u"(^|[^[:alnum:]_])%s" % prefix
345
346     return Q(**kwargs)
347
348
349 def _word_starts_with_regexp(prefix):
350     prefix = _no_diacritics_regexp(unicode_re_escape(prefix))
351     return ur"(^|(?<=[^\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]))%s" % prefix
352
353
354 def _sqlite_word_starts_with(name, prefix):
355     """ version of _word_starts_with for SQLite
356
357     SQLite in Django uses Python re module
358     """
359     kwargs = {}
360     kwargs['%s__iregex' % name] = _word_starts_with_regexp(prefix)
361     return Q(**kwargs)
362
363
364 if hasattr(settings, 'DATABASES'):
365     if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
366         _word_starts_with = _sqlite_word_starts_with
367 elif settings.DATABASE_ENGINE == 'sqlite3':
368     _word_starts_with = _sqlite_word_starts_with
369
370
371 class App():
372     def __init__(self, name, view):
373         self.name = name
374         self._view = view
375         self.lower = name.lower()
376         self.category = 'application'
377     def view(self):
378         return reverse(*self._view)
379
380 _apps = (
381     App(u'Leśmianator', (u'lesmianator', )),
382     )
383
384
385 def _tags_starting_with(prefix, user=None):
386     prefix = prefix.lower()
387     # PD counter
388     book_stubs = pdcounter_models.BookStub.objects.filter(_word_starts_with('title', prefix))
389     authors = pdcounter_models.Author.objects.filter(_word_starts_with('name', prefix))
390
391     books = models.Book.objects.filter(_word_starts_with('title', prefix))
392     tags = models.Tag.objects.filter(_word_starts_with('name', prefix))
393     if user and user.is_authenticated():
394         tags = tags.filter(~Q(category='book') & (~Q(category='set') | Q(user=user)))
395     else:
396         tags = tags.filter(~Q(category='book') & ~Q(category='set'))
397
398     prefix_regexp = re.compile(_word_starts_with_regexp(prefix))
399     return list(books) + list(tags) + [app for app in _apps if prefix_regexp.search(app.lower)] + list(book_stubs) + list(authors)
400
401
402 def _get_result_link(match, tag_list):
403     if isinstance(match, models.Tag):
404         return reverse('catalogue.views.tagged_object_list',
405             kwargs={'tags': '/'.join(tag.url_chunk for tag in tag_list + [match])}
406         )
407     elif isinstance(match, App):
408         return match.view()
409     else:
410         return match.get_absolute_url()
411
412
413 def _get_result_type(match):
414     if isinstance(match, models.Book) or isinstance(match, pdcounter_models.BookStub):
415         type = 'book'
416     else:
417         type = match.category
418     return type
419
420
421 def books_starting_with(prefix):
422     prefix = prefix.lower()
423     return models.Book.objects.filter(_word_starts_with('title', prefix))
424
425
426 def find_best_matches(query, user=None):
427     """ Finds a Book, Tag, BookStub or Author best matching a query.
428
429     Returns a with:
430       - zero elements when nothing is found,
431       - one element when a best result is found,
432       - more then one element on multiple exact matches
433
434     Raises a ValueError on too short a query.
435     """
436
437     query = query.lower()
438     if len(query) < 2:
439         raise ValueError("query must have at least two characters")
440
441     result = tuple(_tags_starting_with(query, user))
442     # remove pdcounter stuff
443     book_titles = set(match.pretty_title().lower() for match in result
444                       if isinstance(match, models.Book))
445     authors = set(match.name.lower() for match in result
446                   if isinstance(match, models.Tag) and match.category=='author')
447     result = tuple(res for res in result if not (
448                  (isinstance(res, pdcounter_models.BookStub) and res.pretty_title().lower() in book_titles)
449                  or (isinstance(res, pdcounter_models.Author) and res.name.lower() in authors)
450              ))
451
452     exact_matches = tuple(res for res in result if res.name.lower() == query)
453     if exact_matches:
454         return exact_matches
455     else:
456         return tuple(result)[:1]
457
458
459 def search(request):
460     tags = request.GET.get('tags', '')
461     prefix = request.GET.get('q', '')
462
463     try:
464         tag_list = models.Tag.get_tag_list(tags)
465     except:
466         tag_list = []
467
468     try:
469         result = find_best_matches(prefix, request.user)
470     except ValueError:
471         return render_to_response('catalogue/search_too_short.html', {'tags':tag_list, 'prefix':prefix},
472             context_instance=RequestContext(request))
473
474     if len(result) == 1:
475         return HttpResponseRedirect(_get_result_link(result[0], tag_list))
476     elif len(result) > 1:
477         return render_to_response('catalogue/search_multiple_hits.html',
478             {'tags':tag_list, 'prefix':prefix, 'results':((x, _get_result_link(x, tag_list), _get_result_type(x)) for x in result)},
479             context_instance=RequestContext(request))
480     else:
481         form = PublishingSuggestForm(initial={"books": prefix + ", "})
482         return render_to_response('catalogue/search_no_hits.html', 
483             {'tags':tag_list, 'prefix':prefix, "pubsuggest_form": form},
484             context_instance=RequestContext(request))
485
486
487 def tags_starting_with(request):
488     prefix = request.GET.get('q', '')
489     # Prefix must have at least 2 characters
490     if len(prefix) < 2:
491         return HttpResponse('')
492     tags_list = []
493     result = ""   
494     for tag in _tags_starting_with(prefix, request.user):
495         if not tag.name in tags_list:
496             result += "\n" + tag.name
497             tags_list.append(tag.name)
498     return HttpResponse(result)
499
500 def json_tags_starting_with(request, callback=None):
501     # Callback for JSONP
502     prefix = request.GET.get('q', '')
503     callback = request.GET.get('callback', '')
504     # Prefix must have at least 2 characters
505     if len(prefix) < 2:
506         return HttpResponse('')
507     tags_list = []
508     for tag in _tags_starting_with(prefix, request.user):
509         if not tag.name in tags_list:
510             tags_list.append(tag.name)
511     if request.GET.get('mozhint', ''):
512         result = [prefix, tags_list]
513     else:
514         result = {"matches": tags_list}
515     return JSONResponse(result, callback)
516
517 # ====================
518 # = Shelf management =
519 # ====================
520 @login_required
521 @cache.never_cache
522 def user_shelves(request):
523     shelves = models.Tag.objects.filter(category='set', user=request.user)
524     new_set_form = forms.NewSetForm()
525     return render_to_response('catalogue/user_shelves.html', locals(),
526             context_instance=RequestContext(request))
527
528 @cache.never_cache
529 def book_sets(request, slug):
530     if not request.user.is_authenticated():
531         return HttpResponse(_('<p>To maintain your shelves you need to be logged in.</p>'))
532
533     book = get_object_or_404(models.Book, slug=slug)
534     user_sets = models.Tag.objects.filter(category='set', user=request.user)
535     book_sets = book.tags.filter(category='set', user=request.user)
536
537     if request.method == 'POST':
538         form = forms.ObjectSetsForm(book, request.user, request.POST)
539         if form.is_valid():
540             old_shelves = list(book.tags.filter(category='set'))
541             new_shelves = [models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']]
542
543             for shelf in [shelf for shelf in old_shelves if shelf not in new_shelves]:
544                 shelf.book_count = None
545                 shelf.save()
546
547             for shelf in [shelf for shelf in new_shelves if shelf not in old_shelves]:
548                 shelf.book_count = None
549                 shelf.save()
550
551             book.tags = new_shelves + list(book.tags.filter(~Q(category='set') | ~Q(user=request.user)))
552             if request.is_ajax():
553                 return JSONResponse('{"msg":"'+_("<p>Shelves were sucessfully saved.</p>")+'", "after":"close"}')
554             else:
555                 return HttpResponseRedirect('/')
556     else:
557         form = forms.ObjectSetsForm(book, request.user)
558         new_set_form = forms.NewSetForm()
559
560     return render_to_response('catalogue/book_sets.html', locals(),
561         context_instance=RequestContext(request))
562
563
564 @login_required
565 @require_POST
566 @cache.never_cache
567 def remove_from_shelf(request, shelf, book):
568     book = get_object_or_404(models.Book, slug=book)
569     shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user)
570
571     if shelf in book.tags:
572         models.Tag.objects.remove_tag(book, shelf)
573
574         shelf.book_count = None
575         shelf.save()
576
577         return HttpResponse(_('Book was successfully removed from the shelf'))
578     else:
579         return HttpResponse(_('This book is not on the shelf'))
580
581
582 def collect_books(books):
583     """
584     Returns all real books in collection.
585     """
586     result = []
587     for book in books:
588         if len(book.children.all()) == 0:
589             result.append(book)
590         else:
591             result += collect_books(book.children.all())
592     return result
593
594
595 @cache.never_cache
596 def download_shelf(request, slug):
597     """"
598     Create a ZIP archive on disk and transmit it in chunks of 8KB,
599     without loading the whole file into memory. A similar approach can
600     be used for large dynamic PDF files.
601     """
602     shelf = get_object_or_404(models.Tag, slug=slug, category='set')
603
604     formats = []
605     form = forms.DownloadFormatsForm(request.GET)
606     if form.is_valid():
607         formats = form.cleaned_data['formats']
608     if len(formats) == 0:
609         formats = ['pdf', 'epub', 'odt', 'txt']
610
611     # Create a ZIP archive
612     temp = tempfile.TemporaryFile()
613     archive = zipfile.ZipFile(temp, 'w')
614
615     already = set()
616     for book in collect_books(models.Book.tagged.with_all(shelf)):
617         if 'pdf' in formats and book.pdf_file:
618             filename = book.pdf_file.path
619             archive.write(filename, str('%s.pdf' % book.slug))
620         if book.root_ancestor not in already and 'epub' in formats and book.root_ancestor.epub_file:
621             filename = book.root_ancestor.epub_file.path
622             archive.write(filename, str('%s.epub' % book.root_ancestor.slug))
623             already.add(book.root_ancestor)
624         if 'odt' in formats and book.has_media("odt"):
625             for file in book.get_media("odt"):
626                 filename = file.file.path
627                 archive.write(filename, str('%s.odt' % slughifi(file.name)))
628         if 'txt' in formats and book.txt_file:
629             filename = book.txt_file.path
630             archive.write(filename, str('%s.txt' % book.slug))
631     archive.close()
632
633     response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed')
634     response['Content-Disposition'] = 'attachment; filename=%s.zip' % slughifi(shelf.name)
635     response['Content-Length'] = temp.tell()
636
637     temp.seek(0)
638     response.write(temp.read())
639     return response
640
641
642 @cache.never_cache
643 def shelf_book_formats(request, shelf):
644     """"
645     Returns a list of formats of books in shelf.
646     """
647     shelf = get_object_or_404(models.Tag, slug=shelf, category='set')
648
649     formats = {'pdf': False, 'epub': False, 'odt': False, 'txt': False}
650
651     for book in collect_books(models.Book.tagged.with_all(shelf)):
652         if book.pdf_file:
653             formats['pdf'] = True
654         if book.root_ancestor.epub_file:
655             formats['epub'] = True
656         if book.txt_file:
657             formats['txt'] = True
658         for format in ('odt',):
659             if book.has_media(format):
660                 formats[format] = True
661
662     return HttpResponse(LazyEncoder().encode(formats))
663
664
665 @login_required
666 @require_POST
667 @cache.never_cache
668 def new_set(request):
669     new_set_form = forms.NewSetForm(request.POST)
670     if new_set_form.is_valid():
671         new_set = new_set_form.save(request.user)
672
673         if request.is_ajax():
674             return JSONResponse('{"id":"%d", "name":"%s", "msg":"<p>Shelf <strong>%s</strong> was successfully created</p>"}' % (new_set.id, new_set.name, new_set))
675         else:
676             return HttpResponseRedirect('/')
677
678     return HttpResponseRedirect('/')
679
680
681 @login_required
682 @require_POST
683 @cache.never_cache
684 def delete_shelf(request, slug):
685     user_set = get_object_or_404(models.Tag, slug=slug, category='set', user=request.user)
686     user_set.delete()
687
688     if request.is_ajax():
689         return HttpResponse(_('<p>Shelf <strong>%s</strong> was successfully removed</p>') % user_set.name)
690     else:
691         return HttpResponseRedirect('/')
692
693
694 # ==================
695 # = Authentication =
696 # ==================
697 @require_POST
698 @cache.never_cache
699 def login(request):
700     form = AuthenticationForm(data=request.POST, prefix='login')
701     if form.is_valid():
702         auth.login(request, form.get_user())
703         response_data = {'success': True, 'errors': {}}
704     else:
705         response_data = {'success': False, 'errors': form.errors}
706     return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
707
708
709 @require_POST
710 @cache.never_cache
711 def register(request):
712     registration_form = UserCreationForm(request.POST, prefix='registration')
713     if registration_form.is_valid():
714         user = registration_form.save()
715         user = auth.authenticate(
716             username=registration_form.cleaned_data['username'],
717             password=registration_form.cleaned_data['password1']
718         )
719         auth.login(request, user)
720         response_data = {'success': True, 'errors': {}}
721     else:
722         response_data = {'success': False, 'errors': registration_form.errors}
723     return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
724
725
726 @cache.never_cache
727 def logout_then_redirect(request):
728     auth.logout(request)
729     return HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
730
731
732
733 # =========
734 # = Admin =
735 # =========
736 @login_required
737 @staff_required
738 def import_book(request):
739     """docstring for import_book"""
740     book_import_form = forms.BookImportForm(request.POST, request.FILES)
741     if book_import_form.is_valid():
742         try:
743             book_import_form.save()
744         except:
745             info = sys.exc_info()
746             exception = pprint.pformat(info[1])
747             tb = '\n'.join(traceback.format_tb(info[2]))
748             return HttpResponse(_("An error occurred: %(exception)s\n\n%(tb)s") % {'exception':exception, 'tb':tb}, mimetype='text/plain')
749         return HttpResponse(_("Book imported successfully"))
750     else:
751         return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
752
753
754
755 def clock(request):
756     """ Provides server time for jquery.countdown,
757     in a format suitable for Date.parse()
758     """
759     return HttpResponse(datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
760
761
762 # info views for API
763
764 def book_info(request, id, lang='pl'):
765     book = get_object_or_404(models.Book, id=id)
766     # set language by hand
767     translation.activate(lang)
768     return render_to_response('catalogue/book_info.html', locals(),
769         context_instance=RequestContext(request))
770
771 def tag_info(request, id):
772     tag = get_object_or_404(models.Tag, id=id)
773     return HttpResponse(tag.description)