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