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