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.
13 from operator import itemgetter
14 from datetime import datetime
16 from django.conf import settings
17 from django.template import RequestContext
18 from django.shortcuts import render_to_response, get_object_or_404
19 from django.http import HttpResponse, HttpResponseRedirect, Http404
20 from django.core.urlresolvers import reverse
21 from django.db.models import Q
22 from django.contrib.auth.decorators import login_required, user_passes_test
23 from django.utils.datastructures import SortedDict
24 from django.views.decorators.http import require_POST
25 from django.contrib import auth
26 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
27 from django.utils import simplejson
28 from django.utils.functional import Promise
29 from django.utils.encoding import force_unicode
30 from django.utils.http import urlquote_plus
31 from django.views.decorators import cache
32 from django.utils.translation import ugettext as _
33 from django.views.generic.list_detail import object_list
34 from django.template.defaultfilters import slugify
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 slughifi import slughifi
44 staff_required = user_passes_test(lambda user: user.is_staff)
47 class LazyEncoder(simplejson.JSONEncoder):
48 def default(self, obj):
49 if isinstance(obj, Promise):
50 return force_unicode(obj)
53 # shortcut for JSON reponses
54 class JSONResponse(HttpResponse):
55 def __init__(self, data={}, callback=None, **kwargs):
57 kwargs.pop('mimetype', None)
58 data = simplejson.dumps(data)
60 data = callback + "(" + data + ");"
61 super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs)
64 def main_page(request):
65 if request.user.is_authenticated():
66 shelves = models.Tag.objects.filter(category='set', user=request.user)
67 new_set_form = forms.NewSetForm()
69 tags = models.Tag.objects.exclude(category__in=('set', 'book'))
71 tag.count = tag.get_count()
72 categories = split_tags(tags)
73 fragment_tags = categories.get('theme', [])
75 form = forms.SearchForm()
76 return render_to_response('catalogue/main_page.html', locals(),
77 context_instance=RequestContext(request))
80 def book_list(request, filter=None, template_name='catalogue/book_list.html'):
81 """ generates a listing of all books, optionally filtered with a test function """
83 form = forms.SearchForm()
86 books = models.Book.objects.all().order_by('parent_number', 'title').only('title', 'parent', 'slug')
88 books = books.filter(filter).distinct()
89 book_ids = set((book.pk for book in books))
91 parent = book.parent_id
92 if parent not in book_ids:
94 books_by_parent.setdefault(parent, []).append(book)
97 books_by_parent.setdefault(book.parent_id, []).append(book)
100 books_by_author = SortedDict()
101 books_nav = SortedDict()
102 for tag in models.Tag.objects.filter(category='author'):
103 books_by_author[tag] = []
105 for book in books_by_parent.get(None,()):
106 authors = list(book.tags.filter(category='author'))
108 for author in authors:
109 books_by_author[author].append(book)
113 for tag in books_by_author:
114 if books_by_author[tag]:
115 books_nav.setdefault(tag.sort_key[0], []).append(tag)
117 return render_to_response(template_name, locals(),
118 context_instance=RequestContext(request))
121 def audiobook_list(request):
122 return book_list(request, Q(medias__type='mp3') | Q(medias__type='ogg'),
123 template_name='catalogue/audiobook_list.html')
126 def daisy_list(request):
127 return book_list(request, Q(medias__type='daisy'),
128 template_name='catalogue/daisy_list.html')
131 def differentiate_tags(request, tags, ambiguous_slugs):
132 beginning = '/'.join(tag.url_chunk for tag in tags)
133 unparsed = '/'.join(ambiguous_slugs[1:])
135 for tag in models.Tag.objects.exclude(category='book').filter(slug=ambiguous_slugs[0]):
137 'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
140 return render_to_response('catalogue/differentiate_tags.html',
141 {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]},
142 context_instance=RequestContext(request))
145 def tagged_object_list(request, tags=''):
147 tags = models.Tag.get_tag_list(tags)
148 except models.Tag.DoesNotExist:
149 chunks = tags.split('/')
150 if len(chunks) == 2 and chunks[0] == 'autor':
151 return pdcounter_views.author_detail(request, chunks[1])
154 except models.Tag.MultipleObjectsReturned, e:
155 return differentiate_tags(request, e.tags, e.ambiguous_slugs)
158 if len(tags) > settings.MAX_TAG_LIST:
160 except AttributeError:
163 if len([tag for tag in tags if tag.category == 'book']):
166 theme_is_set = [tag for tag in tags if tag.category == 'theme']
167 shelf_is_set = [tag for tag in tags if tag.category == 'set']
168 only_shelf = shelf_is_set and len(tags) == 1
169 only_my_shelf = only_shelf and request.user.is_authenticated() and request.user == tags[0].user
171 objects = only_author = None
175 shelf_tags = [tag for tag in tags if tag.category == 'set']
176 fragment_tags = [tag for tag in tags if tag.category != 'set']
177 fragments = models.Fragment.tagged.with_all(fragment_tags)
180 books = models.Book.tagged.with_all(shelf_tags).order_by()
181 l_tags = models.Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books])
182 fragments = models.Fragment.tagged.with_any(l_tags, fragments)
184 # newtagging goes crazy if we just try:
185 #related_tags = models.Tag.objects.usage_for_queryset(fragments, counts=True,
186 # extra={'where': ["catalogue_tag.category != 'book'"]})
187 fragment_keys = [fragment.pk for fragment in fragments]
189 related_tags = models.Fragment.tags.usage(counts=True,
190 filters={'pk__in': fragment_keys},
191 extra={'where': ["catalogue_tag.category != 'book'"]})
192 related_tags = (tag for tag in related_tags if tag not in fragment_tags)
193 categories = split_tags(related_tags)
197 # get relevant books and their tags
198 objects = models.Book.tagged.with_all(tags)
200 # eliminate descendants
201 l_tags = models.Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in objects])
202 descendants_keys = [book.pk for book in models.Book.tagged.with_any(l_tags)]
204 objects = objects.exclude(pk__in=descendants_keys)
206 # get related tags from `tag_counter` and `theme_counter`
208 tags_pks = [tag.pk for tag in tags]
210 for tag_pk, value in itertools.chain(book.tag_counter.iteritems(), book.theme_counter.iteritems()):
211 if tag_pk in tags_pks:
213 related_counts[tag_pk] = related_counts.get(tag_pk, 0) + value
214 related_tags = models.Tag.objects.filter(pk__in=related_counts.keys())
215 related_tags = [tag for tag in related_tags if tag not in tags]
216 for tag in related_tags:
217 tag.count = related_counts[tag.pk]
219 categories = split_tags(related_tags)
223 only_author = len(tags) == 1 and tags[0].category == 'author'
224 objects = models.Book.objects.none()
229 template_name='catalogue/tagged_object_list.html',
231 'categories': categories,
232 'only_shelf': only_shelf,
233 'only_author': only_author,
234 'only_my_shelf': only_my_shelf,
235 'formats_form': forms.DownloadFormatsForm(),
242 def book_fragments(request, book_slug, theme_slug):
243 book = get_object_or_404(models.Book, slug=book_slug)
244 book_tag = get_object_or_404(models.Tag, slug='l-' + book_slug, category='book')
245 theme = get_object_or_404(models.Tag, slug=theme_slug, category='theme')
246 fragments = models.Fragment.tagged.with_all([book_tag, theme])
248 form = forms.SearchForm()
249 return render_to_response('catalogue/book_fragments.html', locals(),
250 context_instance=RequestContext(request))
253 def book_detail(request, slug):
255 book = models.Book.objects.get(slug=slug)
256 except models.Book.DoesNotExist:
257 return pdcounter_views.book_stub_detail(request, slug)
259 book_tag = book.book_tag()
260 tags = list(book.tags.filter(~Q(category='set')))
261 categories = split_tags(tags)
262 book_children = book.children.all().order_by('parent_number', 'title')
267 parents.append(_book.parent)
269 parents = reversed(parents)
271 theme_counter = book.theme_counter
272 book_themes = models.Tag.objects.filter(pk__in=theme_counter.keys())
273 for tag in book_themes:
274 tag.count = theme_counter[tag.pk]
276 extra_info = book.get_extra_info_value()
278 form = forms.SearchForm()
279 return render_to_response('catalogue/book_detail.html', locals(),
280 context_instance=RequestContext(request))
283 def book_text(request, slug):
284 book = get_object_or_404(models.Book, slug=slug)
285 if not book.has_html_file():
288 for fragment in book.fragments.all():
289 for theme in fragment.tags.filter(category='theme'):
290 book_themes.setdefault(theme, []).append(fragment)
292 book_themes = book_themes.items()
293 book_themes.sort(key=lambda s: s[0].sort_key)
294 return render_to_response('catalogue/book_text.html', locals(),
295 context_instance=RequestContext(request))
302 def _no_diacritics_regexp(query):
303 """ returns a regexp for searching for a query without diacritics
305 should be locale-aware """
307 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źżŹŻ',
308 u'ą':u'ąĄ', u'ć':u'ćĆ', u'ę':u'ęĘ', u'ł': u'łŁ', u'ń':u'ńŃ', u'ó':u'óÓ', u'ś':u'śŚ', u'ź':u'źŹ', u'ż':u'żŻ'
312 return u"(%s)" % '|'.join(names[l])
313 return re.sub(u'[%s]' % (u''.join(names.keys())), repl, query)
315 def unicode_re_escape(query):
316 """ Unicode-friendly version of re.escape """
317 return re.sub('(?u)(\W)', r'\\\1', query)
319 def _word_starts_with(name, prefix):
320 """returns a Q object getting models having `name` contain a word
321 starting with `prefix`
323 We define word characters as alphanumeric and underscore, like in JS.
325 Works for MySQL, PostgreSQL, Oracle.
326 For SQLite, _sqlite* version is substituted for this.
330 prefix = _no_diacritics_regexp(unicode_re_escape(prefix))
331 # can't use [[:<:]] (word start),
332 # but we want both `xy` and `(xy` to catch `(xyz)`
333 kwargs['%s__iregex' % name] = u"(^|[^[:alnum:]_])%s" % prefix
338 def _word_starts_with_regexp(prefix):
339 prefix = _no_diacritics_regexp(unicode_re_escape(prefix))
340 return ur"(^|(?<=[^\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]))%s" % prefix
343 def _sqlite_word_starts_with(name, prefix):
344 """ version of _word_starts_with for SQLite
346 SQLite in Django uses Python re module
349 kwargs['%s__iregex' % name] = _word_starts_with_regexp(prefix)
353 if hasattr(settings, 'DATABASES'):
354 if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
355 _word_starts_with = _sqlite_word_starts_with
356 elif settings.DATABASE_ENGINE == 'sqlite3':
357 _word_starts_with = _sqlite_word_starts_with
361 def __init__(self, name, view):
364 self.lower = name.lower()
365 self.category = 'application'
367 return reverse(*self._view)
370 App(u'Leśmianator', (u'lesmianator', )),
374 def _tags_starting_with(prefix, user=None):
375 prefix = prefix.lower()
377 book_stubs = pdcounter_models.BookStub.objects.filter(_word_starts_with('title', prefix))
378 authors = pdcounter_models.Author.objects.filter(_word_starts_with('name', prefix))
380 books = models.Book.objects.filter(_word_starts_with('title', prefix))
381 tags = models.Tag.objects.filter(_word_starts_with('name', prefix))
382 if user and user.is_authenticated():
383 tags = tags.filter(~Q(category='book') & (~Q(category='set') | Q(user=user)))
385 tags = tags.filter(~Q(category='book') & ~Q(category='set'))
387 prefix_regexp = re.compile(_word_starts_with_regexp(prefix))
388 return list(books) + list(tags) + [app for app in _apps if prefix_regexp.search(app.lower)] + list(book_stubs) + list(authors)
391 def _get_result_link(match, tag_list):
392 if isinstance(match, models.Tag):
393 return reverse('catalogue.views.tagged_object_list',
394 kwargs={'tags': '/'.join(tag.url_chunk for tag in tag_list + [match])}
396 elif isinstance(match, App):
399 return match.get_absolute_url()
402 def _get_result_type(match):
403 if isinstance(match, models.Book) or isinstance(match, pdcounter_models.BookStub):
406 type = match.category
410 def books_starting_with(prefix):
411 prefix = prefix.lower()
412 return models.Book.objects.filter(_word_starts_with('title', prefix))
415 def find_best_matches(query, user=None):
416 """ Finds a Book, Tag, BookStub or Author best matching a query.
419 - zero elements when nothing is found,
420 - one element when a best result is found,
421 - more then one element on multiple exact matches
423 Raises a ValueError on too short a query.
426 query = query.lower()
428 raise ValueError("query must have at least two characters")
430 result = tuple(_tags_starting_with(query, user))
431 exact_matches = tuple(res for res in result if res.name.lower() == query)
439 tags = request.GET.get('tags', '')
440 prefix = request.GET.get('q', '')
443 tag_list = models.Tag.get_tag_list(tags)
448 result = find_best_matches(prefix, request.user)
450 return render_to_response('catalogue/search_too_short.html', {'tags':tag_list, 'prefix':prefix},
451 context_instance=RequestContext(request))
454 return HttpResponseRedirect(_get_result_link(result[0], tag_list))
455 elif len(result) > 1:
456 return render_to_response('catalogue/search_multiple_hits.html',
457 {'tags':tag_list, 'prefix':prefix, 'results':((x, _get_result_link(x, tag_list), _get_result_type(x)) for x in result)},
458 context_instance=RequestContext(request))
460 return render_to_response('catalogue/search_no_hits.html', {'tags':tag_list, 'prefix':prefix},
461 context_instance=RequestContext(request))
464 def tags_starting_with(request):
465 prefix = request.GET.get('q', '')
466 # Prefix must have at least 2 characters
468 return HttpResponse('')
471 for tag in _tags_starting_with(prefix, request.user):
472 if not tag.name in tags_list:
473 result += "\n" + tag.name
474 tags_list.append(tag.name)
475 return HttpResponse(result)
477 def json_tags_starting_with(request, callback=None):
479 prefix = request.GET.get('q', '')
480 callback = request.GET.get('callback', '')
481 # Prefix must have at least 2 characters
483 return HttpResponse('')
486 for tag in _tags_starting_with(prefix, request.user):
487 if not tag.name in tags_list:
488 result += "\n" + tag.name
489 tags_list.append(tag.name)
490 dict_result = {"matches": tags_list}
491 return JSONResponse(dict_result, callback)
493 # ====================
494 # = Shelf management =
495 # ====================
498 def user_shelves(request):
499 shelves = models.Tag.objects.filter(category='set', user=request.user)
500 new_set_form = forms.NewSetForm()
501 return render_to_response('catalogue/user_shelves.html', locals(),
502 context_instance=RequestContext(request))
505 def book_sets(request, slug):
506 if not request.user.is_authenticated():
507 return HttpResponse(_('<p>To maintain your shelves you need to be logged in.</p>'))
509 book = get_object_or_404(models.Book, slug=slug)
510 user_sets = models.Tag.objects.filter(category='set', user=request.user)
511 book_sets = book.tags.filter(category='set', user=request.user)
513 if request.method == 'POST':
514 form = forms.ObjectSetsForm(book, request.user, request.POST)
516 old_shelves = list(book.tags.filter(category='set'))
517 new_shelves = [models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']]
519 for shelf in [shelf for shelf in old_shelves if shelf not in new_shelves]:
520 shelf.book_count = None
523 for shelf in [shelf for shelf in new_shelves if shelf not in old_shelves]:
524 shelf.book_count = None
527 book.tags = new_shelves + list(book.tags.filter(~Q(category='set') | ~Q(user=request.user)))
528 if request.is_ajax():
529 return JSONResponse('{"msg":"'+_("<p>Shelves were sucessfully saved.</p>")+'", "after":"close"}')
531 return HttpResponseRedirect('/')
533 form = forms.ObjectSetsForm(book, request.user)
534 new_set_form = forms.NewSetForm()
536 return render_to_response('catalogue/book_sets.html', locals(),
537 context_instance=RequestContext(request))
543 def remove_from_shelf(request, shelf, book):
544 book = get_object_or_404(models.Book, slug=book)
545 shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user)
547 if shelf in book.tags:
548 models.Tag.objects.remove_tag(book, shelf)
550 shelf.book_count = None
553 return HttpResponse(_('Book was successfully removed from the shelf'))
555 return HttpResponse(_('This book is not on the shelf'))
558 def collect_books(books):
560 Returns all real books in collection.
564 if len(book.children.all()) == 0:
567 result += collect_books(book.children.all())
572 def download_shelf(request, slug):
574 Create a ZIP archive on disk and transmit it in chunks of 8KB,
575 without loading the whole file into memory. A similar approach can
576 be used for large dynamic PDF files.
578 shelf = get_object_or_404(models.Tag, slug=slug, category='set')
581 form = forms.DownloadFormatsForm(request.GET)
583 formats = form.cleaned_data['formats']
584 if len(formats) == 0:
585 formats = ['pdf', 'epub', 'odt', 'txt', 'mp3', 'ogg', 'daisy']
587 # Create a ZIP archive
588 temp = tempfile.TemporaryFile()
589 archive = zipfile.ZipFile(temp, 'w')
592 for book in collect_books(models.Book.tagged.with_all(shelf)):
593 if 'pdf' in formats and book.pdf_file:
594 filename = book.pdf_file.path
595 archive.write(filename, str('%s.pdf' % book.slug))
596 if book.root_ancestor not in already and 'epub' in formats and book.root_ancestor.epub_file:
597 filename = book.root_ancestor.epub_file.path
598 archive.write(filename, str('%s.epub' % book.root_ancestor.slug))
599 already.add(book.root_ancestor)
600 if 'odt' in formats and book.has_media("odt"):
601 for file in book.get_media("odt"):
602 filename = file.file.path
603 archive.write(filename, str('%s.odt' % slugify(file.name)))
604 if 'txt' in formats and book.txt_file:
605 filename = book.txt_file.path
606 archive.write(filename, str('%s.txt' % book.slug))
607 if 'mp3' in formats and book.has_media("mp3"):
608 for file in book.get_media("mp3"):
609 filename = file.file.path
610 archive.write(filename, str('%s.mp3' % slugify(file.name)))
611 if 'ogg' in formats and book.has_media("ogg"):
612 for file in book.get_media("ogg"):
613 filename = file.file.path
614 archive.write(filename, str('%s.ogg' % slugify(file.name)))
615 if 'daisy' in formats and book.has_media("daisy"):
616 for file in book.get_media("daisy"):
617 filename = file.file.path
618 archive.write(filename, str('%s.daisy' % slugify(file.name)))
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()
626 response.write(temp.read())
631 def shelf_book_formats(request, shelf):
633 Returns a list of formats of books in shelf.
635 shelf = get_object_or_404(models.Tag, slug=shelf, category='set')
637 formats = {'pdf': False, 'epub': False, 'odt': False, 'txt': False, 'mp3': False, 'ogg': False, 'daisy': False}
639 for book in collect_books(models.Book.tagged.with_all(shelf)):
641 formats['pdf'] = True
642 if book.root_ancestor.epub_file:
643 formats['epub'] = True
645 formats['odt'] = True
647 formats['txt'] = True
649 formats['mp3'] = True
651 formats['ogg'] = True
653 formats['daisy'] = True
655 return HttpResponse(LazyEncoder().encode(formats))
661 def new_set(request):
662 new_set_form = forms.NewSetForm(request.POST)
663 if new_set_form.is_valid():
664 new_set = new_set_form.save(request.user)
666 if request.is_ajax():
667 return JSONResponse('{"id":"%d", "name":"%s", "msg":"<p>Shelf <strong>%s</strong> was successfully created</p>"}' % (new_set.id, new_set.name, new_set))
669 return HttpResponseRedirect('/')
671 return HttpResponseRedirect('/')
677 def delete_shelf(request, slug):
678 user_set = get_object_or_404(models.Tag, slug=slug, category='set', user=request.user)
681 if request.is_ajax():
682 return HttpResponse(_('<p>Shelf <strong>%s</strong> was successfully removed</p>') % user_set.name)
684 return HttpResponseRedirect('/')
693 form = AuthenticationForm(data=request.POST, prefix='login')
695 auth.login(request, form.get_user())
696 response_data = {'success': True, 'errors': {}}
698 response_data = {'success': False, 'errors': form.errors}
699 return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
704 def register(request):
705 registration_form = UserCreationForm(request.POST, prefix='registration')
706 if registration_form.is_valid():
707 user = registration_form.save()
708 user = auth.authenticate(
709 username=registration_form.cleaned_data['username'],
710 password=registration_form.cleaned_data['password1']
712 auth.login(request, user)
713 response_data = {'success': True, 'errors': {}}
715 response_data = {'success': False, 'errors': registration_form.errors}
716 return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
720 def logout_then_redirect(request):
722 return HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
731 def import_book(request):
732 """docstring for import_book"""
733 book_import_form = forms.BookImportForm(request.POST, request.FILES)
734 if book_import_form.is_valid():
736 book_import_form.save()
738 info = sys.exc_info()
739 exception = pprint.pformat(info[1])
740 tb = '\n'.join(traceback.format_tb(info[2]))
741 return HttpResponse(_("An error occurred: %(exception)s\n\n%(tb)s") % {'exception':exception, 'tb':tb}, mimetype='text/plain')
742 return HttpResponse(_("Book imported successfully"))
744 return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
749 """ Provides server time for jquery.countdown,
750 in a format suitable for Date.parse()
752 return HttpResponse(datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
758 Create a zip archive with all XML files.
760 temp = tempfile.TemporaryFile()
761 archive = zipfile.ZipFile(temp, 'w')
763 for book in models.Book.objects.all():
764 archive.write(book.xml_file.path, str('%s.xml' % book.slug))
767 response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed')
768 response['Content-Disposition'] = 'attachment; filename=xmls.zip'
769 response['Content-Length'] = temp.tell()
772 response.write(temp.read())
779 Create a tar archive with all EPUB files, segregated to directories.
782 temp = tempfile.TemporaryFile()
783 archive = tarfile.TarFile(fileobj=temp, mode='w')
785 for book in models.Book.objects.exclude(epub_file=''):
786 archive.add(book.epub_file.path, (u'%s/%s.epub' % (book.get_extra_info_value()['author'], book.slug)).encode('utf-8'))
789 response = HttpResponse(content_type='application/tar', mimetype='application/x-tar')
790 response['Content-Disposition'] = 'attachment; filename=epubs.tar'
791 response['Content-Length'] = temp.tell()
794 response.write(temp.read())