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