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