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