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