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