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