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