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