lesmianator 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     for tag in _tags_starting_with(prefix, request.user):
495         if not tag.name in tags_list:
496             tags_list.append(tag.name)
497     if request.GET.get('mozhint', ''):
498         result = [prefix, tags_list]
499     else:
500         result = {"matches": tags_list}
501     return JSONResponse(result, callback)
502
503 # ====================
504 # = Shelf management =
505 # ====================
506 @login_required
507 @cache.never_cache
508 def user_shelves(request):
509     shelves = models.Tag.objects.filter(category='set', user=request.user)
510     new_set_form = forms.NewSetForm()
511     return render_to_response('catalogue/user_shelves.html', locals(),
512             context_instance=RequestContext(request))
513
514 @cache.never_cache
515 def book_sets(request, slug):
516     if not request.user.is_authenticated():
517         return HttpResponse(_('<p>To maintain your shelves you need to be logged in.</p>'))
518
519     book = get_object_or_404(models.Book, slug=slug)
520     user_sets = models.Tag.objects.filter(category='set', user=request.user)
521     book_sets = book.tags.filter(category='set', user=request.user)
522
523     if request.method == 'POST':
524         form = forms.ObjectSetsForm(book, request.user, request.POST)
525         if form.is_valid():
526             old_shelves = list(book.tags.filter(category='set'))
527             new_shelves = [models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']]
528
529             for shelf in [shelf for shelf in old_shelves if shelf not in new_shelves]:
530                 shelf.book_count = None
531                 shelf.save()
532
533             for shelf in [shelf for shelf in new_shelves if shelf not in old_shelves]:
534                 shelf.book_count = None
535                 shelf.save()
536
537             book.tags = new_shelves + list(book.tags.filter(~Q(category='set') | ~Q(user=request.user)))
538             if request.is_ajax():
539                 return JSONResponse('{"msg":"'+_("<p>Shelves were sucessfully saved.</p>")+'", "after":"close"}')
540             else:
541                 return HttpResponseRedirect('/')
542     else:
543         form = forms.ObjectSetsForm(book, request.user)
544         new_set_form = forms.NewSetForm()
545
546     return render_to_response('catalogue/book_sets.html', locals(),
547         context_instance=RequestContext(request))
548
549
550 @login_required
551 @require_POST
552 @cache.never_cache
553 def remove_from_shelf(request, shelf, book):
554     book = get_object_or_404(models.Book, slug=book)
555     shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user)
556
557     if shelf in book.tags:
558         models.Tag.objects.remove_tag(book, shelf)
559
560         shelf.book_count = None
561         shelf.save()
562
563         return HttpResponse(_('Book was successfully removed from the shelf'))
564     else:
565         return HttpResponse(_('This book is not on the shelf'))
566
567
568 def collect_books(books):
569     """
570     Returns all real books in collection.
571     """
572     result = []
573     for book in books:
574         if len(book.children.all()) == 0:
575             result.append(book)
576         else:
577             result += collect_books(book.children.all())
578     return result
579
580
581 @cache.never_cache
582 def download_shelf(request, slug):
583     """"
584     Create a ZIP archive on disk and transmit it in chunks of 8KB,
585     without loading the whole file into memory. A similar approach can
586     be used for large dynamic PDF files.
587     """
588     shelf = get_object_or_404(models.Tag, slug=slug, category='set')
589
590     formats = []
591     form = forms.DownloadFormatsForm(request.GET)
592     if form.is_valid():
593         formats = form.cleaned_data['formats']
594     if len(formats) == 0:
595         formats = ['pdf', 'epub', 'odt', 'txt']
596
597     # Create a ZIP archive
598     temp = tempfile.TemporaryFile()
599     archive = zipfile.ZipFile(temp, 'w')
600
601     already = set()
602     for book in collect_books(models.Book.tagged.with_all(shelf)):
603         if 'pdf' in formats and book.pdf_file:
604             filename = book.pdf_file.path
605             archive.write(filename, str('%s.pdf' % book.slug))
606         if book.root_ancestor not in already and 'epub' in formats and book.root_ancestor.epub_file:
607             filename = book.root_ancestor.epub_file.path
608             archive.write(filename, str('%s.epub' % book.root_ancestor.slug))
609             already.add(book.root_ancestor)
610         if 'odt' in formats and book.has_media("odt"):
611             for file in book.get_media("odt"):
612                 filename = file.file.path
613                 archive.write(filename, str('%s.odt' % slughifi(file.name)))
614         if 'txt' in formats and book.txt_file:
615             filename = book.txt_file.path
616             archive.write(filename, str('%s.txt' % book.slug))
617     archive.close()
618
619     response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed')
620     response['Content-Disposition'] = 'attachment; filename=%s.zip' % slughifi(shelf.name)
621     response['Content-Length'] = temp.tell()
622
623     temp.seek(0)
624     response.write(temp.read())
625     return response
626
627
628 @cache.never_cache
629 def shelf_book_formats(request, shelf):
630     """"
631     Returns a list of formats of books in shelf.
632     """
633     shelf = get_object_or_404(models.Tag, slug=shelf, category='set')
634
635     formats = {'pdf': False, 'epub': False, 'odt': False, 'txt': False}
636
637     for book in collect_books(models.Book.tagged.with_all(shelf)):
638         if book.pdf_file:
639             formats['pdf'] = True
640         if book.root_ancestor.epub_file:
641             formats['epub'] = True
642         if book.txt_file:
643             formats['txt'] = True
644         for format in ('odt',):
645             if book.has_media(format):
646                 formats[format] = True
647
648     return HttpResponse(LazyEncoder().encode(formats))
649
650
651 @login_required
652 @require_POST
653 @cache.never_cache
654 def new_set(request):
655     new_set_form = forms.NewSetForm(request.POST)
656     if new_set_form.is_valid():
657         new_set = new_set_form.save(request.user)
658
659         if request.is_ajax():
660             return JSONResponse('{"id":"%d", "name":"%s", "msg":"<p>Shelf <strong>%s</strong> was successfully created</p>"}' % (new_set.id, new_set.name, new_set))
661         else:
662             return HttpResponseRedirect('/')
663
664     return HttpResponseRedirect('/')
665
666
667 @login_required
668 @require_POST
669 @cache.never_cache
670 def delete_shelf(request, slug):
671     user_set = get_object_or_404(models.Tag, slug=slug, category='set', user=request.user)
672     user_set.delete()
673
674     if request.is_ajax():
675         return HttpResponse(_('<p>Shelf <strong>%s</strong> was successfully removed</p>') % user_set.name)
676     else:
677         return HttpResponseRedirect('/')
678
679
680 # ==================
681 # = Authentication =
682 # ==================
683 @require_POST
684 @cache.never_cache
685 def login(request):
686     form = AuthenticationForm(data=request.POST, prefix='login')
687     if form.is_valid():
688         auth.login(request, form.get_user())
689         response_data = {'success': True, 'errors': {}}
690     else:
691         response_data = {'success': False, 'errors': form.errors}
692     return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
693
694
695 @require_POST
696 @cache.never_cache
697 def register(request):
698     registration_form = UserCreationForm(request.POST, prefix='registration')
699     if registration_form.is_valid():
700         user = registration_form.save()
701         user = auth.authenticate(
702             username=registration_form.cleaned_data['username'],
703             password=registration_form.cleaned_data['password1']
704         )
705         auth.login(request, user)
706         response_data = {'success': True, 'errors': {}}
707     else:
708         response_data = {'success': False, 'errors': registration_form.errors}
709     return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
710
711
712 @cache.never_cache
713 def logout_then_redirect(request):
714     auth.logout(request)
715     return HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
716
717
718
719 # =========
720 # = Admin =
721 # =========
722 @login_required
723 @staff_required
724 def import_book(request):
725     """docstring for import_book"""
726     book_import_form = forms.BookImportForm(request.POST, request.FILES)
727     if book_import_form.is_valid():
728         try:
729             book_import_form.save()
730         except:
731             info = sys.exc_info()
732             exception = pprint.pformat(info[1])
733             tb = '\n'.join(traceback.format_tb(info[2]))
734             return HttpResponse(_("An error occurred: %(exception)s\n\n%(tb)s") % {'exception':exception, 'tb':tb}, mimetype='text/plain')
735         return HttpResponse(_("Book imported successfully"))
736     else:
737         return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
738
739
740
741 def clock(request):
742     """ Provides server time for jquery.countdown,
743     in a format suitable for Date.parse()
744     """
745     return HttpResponse(datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
746
747
748 @cache.never_cache
749 def xmls(request):
750     """"
751     Create a zip archive with all XML files.
752     This should be removed when we have real API.
753     """
754     temp = tempfile.TemporaryFile()
755     archive = zipfile.ZipFile(temp, 'w')
756
757     for book in models.Book.objects.all():
758         archive.write(book.xml_file.path, str('%s.xml' % book.slug))
759     archive.close()
760
761     response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed')
762     response['Content-Disposition'] = 'attachment; filename=xmls.zip'
763     response['Content-Length'] = temp.tell()
764
765     temp.seek(0)
766     response.write(temp.read())
767     return response