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