refactoring: replaced locals() by explicit dicts
[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     else:
323         one_tag = None
324
325     if category is not None:
326         other = Tag.objects.filter(category=category).exclude(pk__in=[t.pk for t in tags])\
327             .exclude(pk__in=[t.pk for t in category_choices])
328         # Filter out empty tags.
329         ct = ContentType.objects.get_for_model(Picture if gallery else Book)
330         other = other.filter(items__content_type=ct).distinct()
331     else:
332         other = []
333
334     return {
335         'one_tag': one_tag,
336         'choices': choices,
337         'tags': tags,
338         'other': other,
339     }
340
341
342 @register.inclusion_tag('catalogue/inline_tag_list.html')
343 def inline_tag_list(tags, choices=None, category=None, gallery=False):
344     return tag_list(tags, choices, category, gallery)
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, gallery=False,
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_str() 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         'gallery': gallery,
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)