lesmianator in search, django 1.1 compatibility
[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 from django.template.defaultfilters import slugify
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).distinct()
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(medias__type='mp3') | Q(medias__type='ogg'),
121                      template_name='catalogue/audiobook_list.html')
122
123
124 def daisy_list(request):
125     return book_list(request, Q(medias__type='daisy'),
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 _word_starts_with_regexp(prefix):
344     prefix = _no_diacritics_regexp(unicode_re_escape(prefix))
345     return ur"(^|(?<=[^\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]))%s" % prefix
346
347
348 def _sqlite_word_starts_with(name, prefix):
349     """ version of _word_starts_with for SQLite
350
351     SQLite in Django uses Python re module
352     """
353     kwargs = {}
354     kwargs['%s__iregex' % name] = _word_starts_with_regexp(prefix)
355     return Q(**kwargs)
356
357
358 if hasattr(settings, 'DATABASES'):
359     if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
360         _word_starts_with = _sqlite_word_starts_with
361 elif settings.DATABASE_ENGINE == 'sqlite3':
362     _word_starts_with = _sqlite_word_starts_with
363
364
365 class App():
366     def __init__(self, name, view):
367         self.name = name
368         self._view = view
369         self.lower = name.lower()
370         self.category = 'application'
371     def view(self):
372         return reverse(*self._view)
373
374 _apps = (
375     App(u'Leśmianator', (u'lesmianator', )),
376     )
377
378
379 def _tags_starting_with(prefix, user=None):
380     prefix = prefix.lower()
381     book_stubs = models.BookStub.objects.filter(_word_starts_with('title', prefix))
382     books = models.Book.objects.filter(_word_starts_with('title', prefix))
383     book_stubs = filter(lambda x: x not in books, book_stubs)
384     tags = models.Tag.objects.filter(_word_starts_with('name', prefix))
385     if user and user.is_authenticated():
386         tags = tags.filter(~Q(category='book') & (~Q(category='set') | Q(user=user)))
387     else:
388         tags = tags.filter(~Q(category='book') & ~Q(category='set'))
389
390     prefix_regexp = re.compile(_word_starts_with_regexp(prefix))
391     return list(books) + list(tags) + list(book_stubs) + [app for app in _apps if prefix_regexp.search(app.lower)]
392
393
394 def _get_result_link(match, tag_list):
395     if isinstance(match, models.Book) or isinstance(match, models.BookStub):
396         return match.get_absolute_url()
397     elif isinstance(match, App):
398         return match.view()
399     else:
400         return reverse('catalogue.views.tagged_object_list',
401             kwargs={'tags': '/'.join(tag.url_chunk for tag in tag_list + [match])}
402         )
403
404 def _get_result_type(match):
405     if isinstance(match, models.Book) or isinstance(match, 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 or Bookstub 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     exact_matches = tuple(res for res in result if res.name.lower() == query)
434     if exact_matches:
435         return exact_matches
436     else:
437         return result[:1]
438
439
440 def search(request):
441     tags = request.GET.get('tags', '')
442     prefix = request.GET.get('q', '')
443
444     try:
445         tag_list = models.Tag.get_tag_list(tags)
446     except:
447         tag_list = []
448
449     try:
450         result = find_best_matches(prefix, request.user)
451     except ValueError:
452         return render_to_response('catalogue/search_too_short.html', {'tags':tag_list, 'prefix':prefix},
453             context_instance=RequestContext(request))
454
455     if len(result) == 1:
456         return HttpResponseRedirect(_get_result_link(result[0], tag_list))
457     elif len(result) > 1:
458         return render_to_response('catalogue/search_multiple_hits.html',
459             {'tags':tag_list, 'prefix':prefix, 'results':((x, _get_result_link(x, tag_list), _get_result_type(x)) for x in result)},
460             context_instance=RequestContext(request))
461     else:
462         return render_to_response('catalogue/search_no_hits.html', {'tags':tag_list, 'prefix':prefix},
463             context_instance=RequestContext(request))
464
465
466 def tags_starting_with(request):
467     prefix = request.GET.get('q', '')
468     # Prefix must have at least 2 characters
469     if len(prefix) < 2:
470         return HttpResponse('')
471     tags_list = []
472     result = ""   
473     for tag in _tags_starting_with(prefix, request.user):
474         if not tag.name in tags_list:
475             result += "\n" + tag.name
476             tags_list.append(tag.name)
477     return HttpResponse(result)
478
479 def json_tags_starting_with(request, callback=None):
480     # Callback for JSONP
481     prefix = request.GET.get('q', '')
482     callback = request.GET.get('callback', '')
483     # Prefix must have at least 2 characters
484     if len(prefix) < 2:
485         return HttpResponse('')
486     tags_list = []
487     result = ""   
488     for tag in _tags_starting_with(prefix, request.user):
489         if not tag.name in tags_list:
490             result += "\n" + tag.name
491             tags_list.append(tag.name)
492     dict_result = {"matches": tags_list}
493     return JSONResponse(dict_result, callback)
494
495 # ====================
496 # = Shelf management =
497 # ====================
498 @login_required
499 @cache.never_cache
500 def user_shelves(request):
501     shelves = models.Tag.objects.filter(category='set', user=request.user)
502     new_set_form = forms.NewSetForm()
503     return render_to_response('catalogue/user_shelves.html', locals(),
504             context_instance=RequestContext(request))
505
506 @cache.never_cache
507 def book_sets(request, slug):
508     if not request.user.is_authenticated():
509         return HttpResponse(_('<p>To maintain your shelves you need to be logged in.</p>'))
510
511     book = get_object_or_404(models.Book, slug=slug)
512     user_sets = models.Tag.objects.filter(category='set', user=request.user)
513     book_sets = book.tags.filter(category='set', user=request.user)
514
515     if request.method == 'POST':
516         form = forms.ObjectSetsForm(book, request.user, request.POST)
517         if form.is_valid():
518             old_shelves = list(book.tags.filter(category='set'))
519             new_shelves = [models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']]
520
521             for shelf in [shelf for shelf in old_shelves if shelf not in new_shelves]:
522                 shelf.book_count = None
523                 shelf.save()
524
525             for shelf in [shelf for shelf in new_shelves if shelf not in old_shelves]:
526                 shelf.book_count = None
527                 shelf.save()
528
529             book.tags = new_shelves + list(book.tags.filter(~Q(category='set') | ~Q(user=request.user)))
530             if request.is_ajax():
531                 return JSONResponse('{"msg":"'+_("<p>Shelves were sucessfully saved.</p>")+'", "after":"close"}')
532             else:
533                 return HttpResponseRedirect('/')
534     else:
535         form = forms.ObjectSetsForm(book, request.user)
536         new_set_form = forms.NewSetForm()
537
538     return render_to_response('catalogue/book_sets.html', locals(),
539         context_instance=RequestContext(request))
540
541
542 @login_required
543 @require_POST
544 @cache.never_cache
545 def remove_from_shelf(request, shelf, book):
546     book = get_object_or_404(models.Book, slug=book)
547     shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user)
548
549     if shelf in book.tags:
550         models.Tag.objects.remove_tag(book, shelf)
551
552         shelf.book_count = None
553         shelf.save()
554
555         return HttpResponse(_('Book was successfully removed from the shelf'))
556     else:
557         return HttpResponse(_('This book is not on the shelf'))
558
559
560 def collect_books(books):
561     """
562     Returns all real books in collection.
563     """
564     result = []
565     for book in books:
566         if len(book.children.all()) == 0:
567             result.append(book)
568         else:
569             result += collect_books(book.children.all())
570     return result
571
572
573 @cache.never_cache
574 def download_shelf(request, slug):
575     """"
576     Create a ZIP archive on disk and transmit it in chunks of 8KB,
577     without loading the whole file into memory. A similar approach can
578     be used for large dynamic PDF files.
579     """
580     shelf = get_object_or_404(models.Tag, slug=slug, category='set')
581
582     formats = []
583     form = forms.DownloadFormatsForm(request.GET)
584     if form.is_valid():
585         formats = form.cleaned_data['formats']
586     if len(formats) == 0:
587         formats = ['pdf', 'epub', 'odt', 'txt', 'mp3', 'ogg', 'daisy']
588
589     # Create a ZIP archive
590     temp = tempfile.TemporaryFile()
591     archive = zipfile.ZipFile(temp, 'w')
592
593     already = set()
594     for book in collect_books(models.Book.tagged.with_all(shelf)):
595         if 'pdf' in formats and book.pdf_file:
596             filename = book.pdf_file.path
597             archive.write(filename, str('%s.pdf' % book.slug))
598         if book.root_ancestor not in already and 'epub' in formats and book.root_ancestor.epub_file:
599             filename = book.root_ancestor.epub_file.path
600             archive.write(filename, str('%s.epub' % book.root_ancestor.slug))
601             already.add(book.root_ancestor)
602         if 'odt' in formats and book.has_media("odt"):
603             for file in book.get_media("odt"):
604                 filename = file.file.path
605                 archive.write(filename, str('%s.odt' % slugify(file.name)))
606         if 'txt' in formats and book.txt_file:
607             filename = book.txt_file.path
608             archive.write(filename, str('%s.txt' % book.slug))
609         if 'mp3' in formats and book.has_media("mp3"):
610             for file in book.get_media("mp3"):
611                 filename = file.file.path
612                 archive.write(filename, str('%s.mp3' % slugify(file.name)))
613         if 'ogg' in formats and book.has_media("ogg"):
614             for file in book.get_media("ogg"):
615                 filename = file.file.path
616                 archive.write(filename, str('%s.ogg' % slugify(file.name)))
617         if 'daisy' in formats and book.has_media("daisy"):
618             for file in book.get_media("daisy"):
619                 filename = file.file.path
620                 archive.write(filename, str('%s.daisy' % slugify(file.name)))                                
621     archive.close()
622
623     response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed')
624     response['Content-Disposition'] = 'attachment; filename=%s.zip' % slughifi(shelf.name)
625     response['Content-Length'] = temp.tell()
626
627     temp.seek(0)
628     response.write(temp.read())
629     return response
630
631
632 @cache.never_cache
633 def shelf_book_formats(request, shelf):
634     """"
635     Returns a list of formats of books in shelf.
636     """
637     shelf = get_object_or_404(models.Tag, slug=shelf, category='set')
638
639     formats = {'pdf': False, 'epub': False, 'odt': False, 'txt': False, 'mp3': False, 'ogg': False, 'daisy': False}
640
641     for book in collect_books(models.Book.tagged.with_all(shelf)):
642         if book.pdf_file:
643             formats['pdf'] = True
644         if book.root_ancestor.epub_file:
645             formats['epub'] = True
646         if book.odt_file:
647             formats['odt'] = True
648         if book.txt_file:
649             formats['txt'] = True
650         if book.mp3_file:
651             formats['mp3'] = True
652         if book.ogg_file:
653             formats['ogg'] = True
654         if book.daisy_file:
655             formats['daisy'] = True
656
657     return HttpResponse(LazyEncoder().encode(formats))
658
659
660 @login_required
661 @require_POST
662 @cache.never_cache
663 def new_set(request):
664     new_set_form = forms.NewSetForm(request.POST)
665     if new_set_form.is_valid():
666         new_set = new_set_form.save(request.user)
667
668         if request.is_ajax():
669             return JSONResponse('{"id":"%d", "name":"%s", "msg":"<p>Shelf <strong>%s</strong> was successfully created</p>"}' % (new_set.id, new_set.name, new_set))
670         else:
671             return HttpResponseRedirect('/')
672
673     return HttpResponseRedirect('/')
674
675
676 @login_required
677 @require_POST
678 @cache.never_cache
679 def delete_shelf(request, slug):
680     user_set = get_object_or_404(models.Tag, slug=slug, category='set', user=request.user)
681     user_set.delete()
682
683     if request.is_ajax():
684         return HttpResponse(_('<p>Shelf <strong>%s</strong> was successfully removed</p>') % user_set.name)
685     else:
686         return HttpResponseRedirect('/')
687
688
689 # ==================
690 # = Authentication =
691 # ==================
692 @require_POST
693 @cache.never_cache
694 def login(request):
695     form = AuthenticationForm(data=request.POST, prefix='login')
696     if form.is_valid():
697         auth.login(request, form.get_user())
698         response_data = {'success': True, 'errors': {}}
699     else:
700         response_data = {'success': False, 'errors': form.errors}
701     return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
702
703
704 @require_POST
705 @cache.never_cache
706 def register(request):
707     registration_form = UserCreationForm(request.POST, prefix='registration')
708     if registration_form.is_valid():
709         user = registration_form.save()
710         user = auth.authenticate(
711             username=registration_form.cleaned_data['username'],
712             password=registration_form.cleaned_data['password1']
713         )
714         auth.login(request, user)
715         response_data = {'success': True, 'errors': {}}
716     else:
717         response_data = {'success': False, 'errors': registration_form.errors}
718     return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
719
720
721 @cache.never_cache
722 def logout_then_redirect(request):
723     auth.logout(request)
724     return HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
725
726
727
728 # =========
729 # = Admin =
730 # =========
731 @login_required
732 @staff_required
733 def import_book(request):
734     """docstring for import_book"""
735     book_import_form = forms.BookImportForm(request.POST, request.FILES)
736     if book_import_form.is_valid():
737         try:
738             book_import_form.save()
739         except:
740             info = sys.exc_info()
741             exception = pprint.pformat(info[1])
742             tb = '\n'.join(traceback.format_tb(info[2]))
743             return HttpResponse(_("An error occurred: %(exception)s\n\n%(tb)s") % {'exception':exception, 'tb':tb}, mimetype='text/plain')
744         return HttpResponse(_("Book imported successfully"))
745     else:
746         return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
747
748
749
750 def clock(request):
751     """ Provides server time for jquery.countdown,
752     in a format suitable for Date.parse()
753     """
754     return HttpResponse(datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
755
756
757 @cache.never_cache
758 def xmls(request):
759     """"
760     Create a zip archive with all XML files.
761     """
762     temp = tempfile.TemporaryFile()
763     archive = zipfile.ZipFile(temp, 'w')
764
765     for book in models.Book.objects.all():
766         archive.write(book.xml_file.path, str('%s.xml' % book.slug))
767     archive.close()
768
769     response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed')
770     response['Content-Disposition'] = 'attachment; filename=xmls.zip'
771     response['Content-Length'] = temp.tell()
772
773     temp.seek(0)
774     response.write(temp.read())
775     return response
776
777
778 @cache.never_cache
779 def epubs(request):
780     """"
781     Create a tar archive with all EPUB files, segregated to directories.
782     """
783
784     temp = tempfile.TemporaryFile()
785     archive = tarfile.TarFile(fileobj=temp, mode='w')
786
787     for book in models.Book.objects.exclude(epub_file=''):
788         archive.add(book.epub_file.path, (u'%s/%s.epub' % (book.get_extra_info_value()['author'], book.slug)).encode('utf-8'))
789     archive.close()
790
791     response = HttpResponse(content_type='application/tar', mimetype='application/x-tar')
792     response['Content-Disposition'] = 'attachment; filename=epubs.tar'
793     response['Content-Length'] = temp.tell()
794
795     temp.seek(0)
796     response.write(temp.read())
797     return response