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