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