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