Merge branch 'dev'
[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     else:
329         other = []
330
331     return {
332         'one_tag': one_tag,
333         'choices': choices,
334         'category_choices': category_choices,
335         'tags': tags,
336         'other': other,
337         'list_type': list_type,
338     }
339
340
341 @register.inclusion_tag('catalogue/inline_tag_list.html')
342 def inline_tag_list(tags, choices=None, category=None, list_type='books'):
343     return tag_list(tags, choices, category, list_type)
344
345
346 @register.inclusion_tag('catalogue/collection_list.html')
347 def collection_list(collections):
348     return {'collections': collections}
349
350
351 @register.inclusion_tag('catalogue/book_info.html')
352 def book_info(book):
353     return {
354         'is_picture': isinstance(book, Picture),
355         'book': book,
356     }
357
358
359 @register.inclusion_tag('catalogue/work-list.html', takes_context=True)
360 def work_list(context, object_list):
361     request = context.get('request')
362     return {'object_list': object_list, 'request': request}
363
364
365 @register.inclusion_tag('catalogue/plain_list.html', takes_context=True)
366 def plain_list(context, object_list, with_initials=True, by_author=False, choice=None, book=None, list_type='books',
367                paged=True, initial_blocks=False):
368     names = [('', [])]
369     last_initial = None
370     if len(object_list) < settings.CATALOGUE_MIN_INITIALS and not by_author:
371         with_initials = False
372         initial_blocks = False
373     for obj in object_list:
374         if with_initials:
375             if by_author:
376                 initial = obj.sort_key_author
377             else:
378                 initial = obj.get_initial().upper()
379             if initial != last_initial:
380                 last_initial = initial
381                 names.append((obj.author_unicode() if by_author else initial, []))
382         names[-1][1].append(obj)
383     return {
384         'paged': paged,
385         'names': names,
386         'initial_blocks': initial_blocks,
387         'book': book,
388         'list_type': list_type,
389         'choice': choice,
390     }
391
392
393 # TODO: These are no longer just books.
394 @register.inclusion_tag('catalogue/related_books.html', takes_context=True)
395 def related_books(context, instance, limit=6, random=1, taken=0):
396     limit -= taken
397     max_books = limit - random
398     is_picture = isinstance(instance, Picture)
399
400     pics_qs = Picture.objects.all()
401     if is_picture:
402         pics_qs = pics_qs.exclude(pk=instance.pk)
403     pics = Picture.tagged.related_to(instance, pics_qs)
404     if pics.exists():
405         # Reserve one spot for an image.
406         max_books -= 1
407
408     books_qs = Book.objects.all()
409     if not is_picture:
410         books_qs = books_qs.exclude(common_slug=instance.common_slug).exclude(ancestor=instance)
411     books = Book.tagged.related_to(instance, books_qs)[:max_books]
412
413     pics = pics[:1 + max_books - books.count()]
414
415     random_excluded_books = [b.pk for b in books]
416     random_excluded_pics = [p.pk for p in pics]
417     (random_excluded_pics if is_picture else random_excluded_books).append(instance.pk)
418
419     return {
420         'request': context['request'],
421         'books': books,
422         'pics': pics,
423         'random': random,
424         'random_excluded_books': random_excluded_books,
425         'random_excluded_pics': random_excluded_pics,
426     }
427
428
429 @register.simple_tag
430 def download_audio(book, daisy=True):
431     links = []
432     if book.has_media('mp3'):
433         links.append("<a href='%s'>%s</a>" % (
434             reverse('download_zip_mp3', args=[book.slug]), BookMedia.formats['mp3'].name))
435     if book.has_media('ogg'):
436         links.append("<a href='%s'>%s</a>" % (
437             reverse('download_zip_ogg', args=[book.slug]), BookMedia.formats['ogg'].name))
438     if daisy and book.has_media('daisy'):
439         for dsy in book.get_media('daisy'):
440             links.append("<a href='%s'>%s</a>" % (dsy.file.url, BookMedia.formats['daisy'].name))
441     return "".join(links)
442
443
444 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
445 def custom_pdf_link_li(book):
446     return {
447         'book': book,
448         'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
449     }
450
451
452 @register.inclusion_tag("catalogue/snippets/license_icon.html")
453 def license_icon(license_url):
454     """Creates a license icon, if the license_url is known."""
455     known = LICENSES.get(license_url)
456     if known is None:
457         return {}
458     return {
459         "license_url": license_url,
460         "icon": "img/licenses/%s.png" % known['icon'],
461         "license_description": known['description'],
462     }
463
464
465 @register.filter
466 def class_name(obj):
467     return obj.__class__.__name__
468
469
470 @register.simple_tag
471 def source_name(url):
472     url = url.lstrip()
473     netloc = urlparse(url).netloc
474     if not netloc:
475         netloc = urlparse('http://' + url).netloc
476     if not netloc:
477         return ''
478     source, created = Source.objects.get_or_create(netloc=netloc)
479     return source.name or netloc
480
481
482 @ssi_variable(register, patch_response=[add_never_cache_headers])
483 def catalogue_random_book(request, exclude_ids):
484     from .. import app_settings
485     if random() < app_settings.RELATED_RANDOM_PICTURE_CHANCE:
486         return None
487     queryset = Book.objects.exclude(pk__in=exclude_ids)
488     count = queryset.count()
489     if count:
490         return queryset[randint(0, count - 1)].pk
491     else:
492         return None
493
494
495 @ssi_variable(register, patch_response=[add_never_cache_headers])
496 def choose_fragment(request, book_id=None, tag_ids=None, unless=False):
497     if unless:
498         return None
499
500     if book_id is not None:
501         fragment = Book.objects.get(pk=book_id).choose_fragment()
502     else:
503         if tag_ids is not None:
504             tags = Tag.objects.filter(pk__in=tag_ids)
505             fragments = Fragment.tagged.with_all(tags).order_by().only('id')
506         else:
507             fragments = Fragment.objects.all().order_by().only('id')
508         fragment_count = fragments.count()
509         fragment = fragments[randint(0, fragment_count - 1)] if fragment_count else None
510     return fragment.pk if fragment is not None else None
511
512
513 @register.filter
514 def strip_tag(html, tag_name):
515     # docelowo może być warto zainstalować BeautifulSoup do takich rzeczy
516     import re
517     return re.sub(r"<.?%s\b[^>]*>" % tag_name, "", html)