db optimizations
[wolnelektury.git] / src / catalogue / templatetags / catalogue_tags.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 from random import randint, random
6 from urlparse import urlparse
7 from django.contrib.contenttypes.models import ContentType
8
9 from django.conf import settings
10 from django import template
11 from django.template import Node, Variable, Template, Context
12 from django.core.urlresolvers import reverse
13 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
14 from django.utils.cache import add_never_cache_headers
15 from django.utils.translation import ugettext as _
16
17 from ssify import ssi_variable
18 from catalogue.models import Book, BookMedia, Fragment, Tag, Source
19 from catalogue.constants import LICENSES
20 from picture.models import Picture
21
22 register = template.Library()
23
24
25 class RegistrationForm(UserCreationForm):
26     def as_ul(self):
27         """Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."""
28         return self._html_output(
29             u'<li>%(errors)s%(label)s %(field)s<span class="help-text">%(help_text)s</span></li>', u'<li>%s</li>',
30             '</li>', u' %s', False)
31
32
33 class LoginForm(AuthenticationForm):
34     def as_ul(self):
35         """Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."""
36         return self._html_output(
37             u'<li>%(errors)s%(label)s %(field)s<span class="help-text">%(help_text)s</span></li>', u'<li>%s</li>',
38             '</li>', u' %s', False)
39
40
41 def iterable(obj):
42     try:
43         iter(obj)
44         return True
45     except TypeError:
46         return False
47
48
49 def capfirst(text):
50     try:
51         return '%s%s' % (text[0].upper(), text[1:])
52     except IndexError:
53         return ''
54
55
56 @register.simple_tag
57 def html_title_from_tags(tags):
58     if len(tags) < 2:
59         return title_from_tags(tags)
60     template = Template("{{ category }}: <a href='{{ tag.get_absolute_url }}'>{{ tag.name }}</a>")
61     return capfirst(",<br/>".join(
62         template.render(Context({'tag': tag, 'category': _(tag.category)})) for tag in tags))
63
64
65 def simple_title(tags):
66     title = []
67     for tag in tags:
68         title.append("%s: %s" % (_(tag.category), tag.name))
69     return capfirst(', '.join(title))
70
71
72 @register.simple_tag
73 def book_title(book, html_links=False):
74     return book.pretty_title(html_links)
75
76
77 @register.simple_tag
78 def book_title_html(book):
79     return book_title(book, html_links=True)
80
81
82 @register.simple_tag
83 def title_from_tags(tags):
84     def split_tags(tags):
85         result = {}
86         for tag in tags:
87             result[tag.category] = tag
88         return result
89
90     # TODO: Remove this after adding flection mechanism
91     return simple_title(tags)
92
93     class Flection(object):
94         def get_case(self, name, flection):
95             return name
96     flection = Flection()
97
98     self = split_tags(tags)
99
100     title = u''
101
102     # Specjalny przypadek oglądania wszystkich lektur na danej półce
103     if len(self) == 1 and 'set' in self:
104         return u'Półka %s' % self['set']
105
106     # Specjalny przypadek "Twórczość w pozytywizmie", wtedy gdy tylko epoka
107     # jest wybrana przez użytkownika
108     if 'epoch' in self and len(self) == 1:
109         text = u'Twórczość w %s' % flection.get_case(unicode(self['epoch']), u'miejscownik')
110         return capfirst(text)
111
112     # Specjalny przypadek "Dramat w twórczości Sofoklesa", wtedy gdy podane
113     # są tylko rodzaj literacki i autor
114     if 'kind' in self and 'author' in self and len(self) == 2:
115         text = u'%s w twórczości %s' % (
116             unicode(self['kind']), flection.get_case(unicode(self['author']), u'dopełniacz'))
117         return capfirst(text)
118
119     # Przypadki ogólniejsze
120     if 'theme' in self:
121         title += u'Motyw %s' % unicode(self['theme'])
122
123     if 'genre' in self:
124         if 'theme' in self:
125             title += u' w %s' % flection.get_case(unicode(self['genre']), u'miejscownik')
126         else:
127             title += unicode(self['genre'])
128
129     if 'kind' in self or 'author' in self or 'epoch' in self:
130         if 'genre' in self or 'theme' in self:
131             if 'kind' in self:
132                 title += u' w %s ' % flection.get_case(unicode(self['kind']), u'miejscownik')
133             else:
134                 title += u' w twórczości '
135         else:
136             title += u'%s ' % unicode(self.get('kind', u'twórczość'))
137
138     if 'author' in self:
139         title += flection.get_case(unicode(self['author']), u'dopełniacz')
140     elif 'epoch' in self:
141         title += flection.get_case(unicode(self['epoch']), u'dopełniacz')
142
143     return capfirst(title)
144
145
146 @register.simple_tag
147 def book_tree(book_list, books_by_parent):
148     text = "".join("<li><a href='%s'>%s</a>%s</li>" % (
149         book.get_absolute_url(), book.title, book_tree(books_by_parent.get(book, ()), books_by_parent)
150         ) for book in book_list)
151
152     if text:
153         return "<ol>%s</ol>" % text
154     else:
155         return ''
156
157
158 @register.simple_tag
159 def audiobook_tree(book_list, books_by_parent):
160     text = "".join("<li><a class='open-player' href='%s'>%s</a>%s</li>" % (
161         reverse("book_player", args=[book.slug]), book.title,
162         audiobook_tree(books_by_parent.get(book, ()), books_by_parent)
163     ) for book in book_list)
164
165     if text:
166         return "<ol>%s</ol>" % text
167     else:
168         return ''
169
170
171 @register.simple_tag
172 def book_tree_texml(book_list, books_by_parent, depth=1):
173     return "".join("""
174             <cmd name='hspace'><parm>%(depth)dem</parm></cmd>%(title)s
175             <spec cat='align' /><cmd name="note"><parm>%(audiences)s</parm></cmd>
176             <spec cat='align' /><cmd name="note"><parm>%(audiobook)s</parm></cmd>
177             <ctrl ch='\\' />
178             %(children)s
179             """ % {
180                 "depth": depth,
181                 "title": book.title,
182                 "audiences": ", ".join(book.audiences_pl()),
183                 "audiobook": "audiobook" if book.has_media('mp3') else "",
184                 "children": book_tree_texml(books_by_parent.get(book.id, ()), books_by_parent, depth + 1)
185             } for book in book_list)
186
187
188 @register.simple_tag
189 def book_tree_csv(author, book_list, books_by_parent, depth=1, max_depth=3, delimeter="\t"):
190     def quote_if_necessary(s):
191         try:
192             s.index(delimeter)
193             s.replace('"', '\\"')
194             return '"%s"' % s
195         except ValueError:
196             return s
197
198     return "".join("""%(author)s%(d)s%(preindent)s%(title)s%(d)s%(postindent)s%(audiences)s%(d)s%(audiobook)s
199 %(children)s""" % {
200                 "d": delimeter,
201                 "preindent": delimeter * (depth - 1),
202                 "postindent": delimeter * (max_depth - depth),
203                 "depth": depth,
204                 "author": quote_if_necessary(author.name),
205                 "title": quote_if_necessary(book.title),
206                 "audiences": ", ".join(book.audiences_pl()),
207                 "audiobook": "audiobook" if book.has_media('mp3') else "",
208                 "children": book_tree_csv(author, books_by_parent.get(book.id, ()), books_by_parent, depth + 1)
209             } for book in book_list)
210
211
212 @register.simple_tag
213 def all_editors(extra_info):
214     editors = []
215     if 'editors' in extra_info:
216         editors += extra_info['editors']
217     if 'technical_editors' in extra_info:
218         editors += extra_info['technical_editors']
219     # support for extra_info-s from librarian<1.2
220     if 'editor' in extra_info:
221         editors.append(extra_info['editor'])
222     if 'technical_editor' in extra_info:
223         editors.append(extra_info['technical_editor'])
224     return ', '.join(
225                      ' '.join(p.strip() for p in person.rsplit(',', 1)[::-1])
226                      for person in sorted(set(editors)))
227
228
229 @register.simple_tag
230 def user_creation_form():
231     return RegistrationForm(prefix='registration').as_ul()
232
233
234 @register.simple_tag
235 def authentication_form():
236     return LoginForm(prefix='login').as_ul()
237
238
239 @register.tag
240 def catalogue_url(parser, token):
241     bits = token.split_contents()
242
243     tags_to_add = []
244     tags_to_remove = []
245     for bit in bits[1:]:
246         if bit[0] == '-':
247             tags_to_remove.append(bit[1:])
248         else:
249             tags_to_add.append(bit)
250
251     return CatalogueURLNode(tags_to_add, tags_to_remove)
252
253
254 @register.tag
255 def catalogue_url_gallery(parser, token):
256     bits = token.split_contents()
257
258     tags_to_add = []
259     tags_to_remove = []
260     for bit in bits[1:]:
261         if bit[0] == '-':
262             tags_to_remove.append(bit[1:])
263         else:
264             tags_to_add.append(bit)
265
266     return CatalogueURLNode(tags_to_add, tags_to_remove, gallery=True)
267
268
269 class CatalogueURLNode(Node):
270     def __init__(self, tags_to_add, tags_to_remove, gallery=False):
271         self.tags_to_add = [Variable(tag) for tag in tags_to_add]
272         self.tags_to_remove = [Variable(tag) for tag in tags_to_remove]
273         self.gallery = gallery
274
275     def render(self, context):
276         tags_to_add = []
277         tags_to_remove = []
278
279         for tag_variable in self.tags_to_add:
280             tag = tag_variable.resolve(context)
281             if isinstance(tag, (list, dict)):
282                 tags_to_add += [t for t in tag]
283             else:
284                 tags_to_add.append(tag)
285
286         for tag_variable in self.tags_to_remove:
287             tag = tag_variable.resolve(context)
288             if iterable(tag):
289                 tags_to_remove += [t for t in tag]
290             else:
291                 tags_to_remove.append(tag)
292
293         tag_slugs = [tag.url_chunk for tag in tags_to_add]
294         for tag in tags_to_remove:
295             try:
296                 tag_slugs.remove(tag.url_chunk)
297             except KeyError:
298                 pass
299
300         if len(tag_slugs) > 0:
301             if self.gallery:
302                 return reverse('tagged_object_list_gallery', kwargs={'tags': '/'.join(tag_slugs)})
303             else:
304                 return reverse('tagged_object_list', kwargs={'tags': '/'.join(tag_slugs)})
305         else:
306             return reverse('book_list')
307
308
309 @register.inclusion_tag('catalogue/tag_list.html')
310 def tag_list(tags, choices=None, category=None, gallery=False):
311     print(tags, choices, category)
312     if choices is None:
313         choices = []
314
315     if category is None and tags:
316         category = tags[0].category
317
318     category_choices = [tag for tag in choices if tag.category == category]
319
320     if len(tags) == 1 and category not in [t.category for t in choices]:
321         one_tag = tags[0]
322
323     if category is not None:
324         other = Tag.objects.filter(category=category).exclude(pk__in=[t.pk for t in tags])\
325             .exclude(pk__in=[t.pk for t in category_choices])
326         # Filter out empty tags.
327         ct = ContentType.objects.get_for_model(Picture if gallery else Book)
328         other = other.filter(items__content_type=ct).distinct()
329
330     return locals()
331
332
333 @register.inclusion_tag('catalogue/inline_tag_list.html')
334 def inline_tag_list(tags, choices=None, category=None, gallery=False):
335     return tag_list(tags, choices, category, gallery)
336
337
338 @register.inclusion_tag('catalogue/collection_list.html')
339 def collection_list(collections):
340     return locals()
341
342
343 @register.inclusion_tag('catalogue/book_info.html')
344 def book_info(book):
345     return {
346         'is_picture': isinstance(book, Picture),
347         'book': book,
348     }
349
350
351 @register.inclusion_tag('catalogue/work-list.html', takes_context=True)
352 def work_list(context, object_list):
353     request = context.get('request')
354     return locals()
355
356
357 @register.inclusion_tag('catalogue/plain_list.html', takes_context=True)
358 def plain_list(context, object_list, with_initials=True, by_author=False, choice=None, book=None, gallery=False,
359                paged=True, initial_blocks=False):
360     names = [('', [])]
361     last_initial = None
362     if len(object_list) < settings.CATALOGUE_MIN_INITIALS and not by_author:
363         with_initials = False
364         initial_blocks = False
365     for obj in object_list:
366         if with_initials:
367             if by_author:
368                 initial = obj.sort_key_author
369             else:
370                 initial = obj.get_initial().upper()
371             if initial != last_initial:
372                 last_initial = initial
373                 names.append((obj.author_unicode() if by_author else initial, []))
374         names[-1][1].append(obj)
375     return locals()
376
377
378 # TODO: These are no longer just books.
379 @register.inclusion_tag('catalogue/related_books.html', takes_context=True)
380 def related_books(context, instance, limit=6, random=1, taken=0):
381     limit -= taken
382     max_books = limit - random
383     is_picture = isinstance(instance, Picture)
384
385     pics_qs = Picture.objects.all()
386     if is_picture:
387         pics_qs = pics_qs.exclude(pk=instance.pk)
388     pics = Picture.tagged.related_to(instance, pics_qs)
389     if pics.exists():
390         # Reserve one spot for an image.
391         max_books -= 1
392
393     books_qs = Book.objects.all()
394     if not is_picture:
395         books_qs = books_qs.exclude(common_slug=instance.common_slug).exclude(ancestor=instance)
396     books = Book.tagged.related_to(instance, books_qs)[:max_books]
397
398     pics = pics[:1 + max_books - books.count()]
399
400     random_excluded_books = [b.pk for b in books]
401     random_excluded_pics = [p.pk for p in pics]
402     (random_excluded_pics if is_picture else random_excluded_books).append(instance.pk)
403
404     return {
405         'request': context['request'],
406         'books': books,
407         'pics': pics,
408         'random': random,
409         'random_excluded_books': random_excluded_books,
410         'random_excluded_pics': random_excluded_pics,
411     }
412
413
414 @register.simple_tag
415 def download_audio(book, daisy=True):
416     links = []
417     if book.has_media('mp3'):
418         links.append("<a href='%s'>%s</a>" % (
419             reverse('download_zip_mp3', args=[book.slug]), BookMedia.formats['mp3'].name))
420     if book.has_media('ogg'):
421         links.append("<a href='%s'>%s</a>" % (
422             reverse('download_zip_ogg', args=[book.slug]), BookMedia.formats['ogg'].name))
423     if daisy and book.has_media('daisy'):
424         for dsy in book.get_media('daisy'):
425             links.append("<a href='%s'>%s</a>" % (dsy.file.url, BookMedia.formats['daisy'].name))
426     return "".join(links)
427
428
429 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
430 def custom_pdf_link_li(book):
431     return {
432         'book': book,
433         'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
434     }
435
436
437 @register.inclusion_tag("catalogue/snippets/license_icon.html")
438 def license_icon(license_url):
439     """Creates a license icon, if the license_url is known."""
440     known = LICENSES.get(license_url)
441     if known is None:
442         return {}
443     return {
444         "license_url": license_url,
445         "icon": "img/licenses/%s.png" % known['icon'],
446         "license_description": known['description'],
447     }
448
449
450 @register.filter
451 def class_name(obj):
452     return obj.__class__.__name__
453
454
455 @register.simple_tag
456 def source_name(url):
457     url = url.lstrip()
458     netloc = urlparse(url).netloc
459     if not netloc:
460         netloc = urlparse('http://' + url).netloc
461     if not netloc:
462         return ''
463     source, created = Source.objects.get_or_create(netloc=netloc)
464     return source.name or netloc
465
466
467 @ssi_variable(register, patch_response=[add_never_cache_headers])
468 def catalogue_random_book(request, exclude_ids):
469     from .. import app_settings
470     if random() < app_settings.RELATED_RANDOM_PICTURE_CHANCE:
471         return None
472     queryset = Book.objects.exclude(pk__in=exclude_ids)
473     count = queryset.count()
474     if count:
475         return queryset[randint(0, count - 1)].pk
476     else:
477         return None
478
479
480 @ssi_variable(register, patch_response=[add_never_cache_headers])
481 def choose_fragment(request, book_id=None, tag_ids=None, unless=False):
482     if unless:
483         return None
484
485     if book_id is not None:
486         fragment = Book.objects.get(pk=book_id).choose_fragment()
487     else:
488         if tag_ids is not None:
489             tags = Tag.objects.filter(pk__in=tag_ids)
490             fragments = Fragment.tagged.with_all(tags).order_by().only('id')
491         else:
492             fragments = Fragment.objects.all().order_by().only('id')
493         fragment_count = fragments.count()
494         fragment = fragments[randint(0, fragment_count - 1)] if fragment_count else None
495     return fragment.pk if fragment is not None else None
496
497
498 @register.filter
499 def strip_tag(html, tag_name):
500     # docelowo może być warto zainstalować BeautifulSoup do takich rzeczy
501     import re
502     return re.sub(r"<.?%s\b[^>]*>" % tag_name, "", html)