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