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