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