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