pep8 and other code-style changes
[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):
360     names = [('', [])]
361     last_initial = None
362     for obj in object_list:
363         if with_initials:
364             if by_author:
365                 initial = obj.sort_key_author
366             else:
367                 initial = obj.get_initial().upper()
368             if initial != last_initial:
369                 last_initial = initial
370                 names.append((obj.author_str() if by_author else initial, []))
371         names[-1][1].append(obj)
372     return locals()
373
374
375 # TODO: These are no longer just books.
376 @register.inclusion_tag('catalogue/related_books.html', takes_context=True)
377 def related_books(context, instance, limit=6, random=1, taken=0):
378     limit -= taken
379     max_books = limit - random
380     is_picture = isinstance(instance, Picture)
381
382     pics_qs = Picture.objects.all()
383     if is_picture:
384         pics_qs = pics_qs.exclude(pk=instance.pk)
385     pics = Picture.tagged.related_to(instance, pics_qs)
386     if pics.exists():
387         # Reserve one spot for an image.
388         max_books -= 1
389
390     books_qs = Book.objects.all()
391     if not is_picture:
392         books_qs = books_qs.exclude(common_slug=instance.common_slug).exclude(ancestor=instance)
393     books = Book.tagged.related_to(instance, books_qs)[:max_books]
394
395     pics = pics[:1 + max_books - books.count()]
396
397     random_excluded_books = [b.pk for b in books]
398     random_excluded_pics = [p.pk for p in pics]
399     (random_excluded_pics if is_picture else random_excluded_books).append(instance.pk)
400
401     return {
402         'request': context['request'],
403         'books': books,
404         'pics': pics,
405         'random': random,
406         'random_excluded_books': random_excluded_books,
407         'random_excluded_pics': random_excluded_pics,
408     }
409
410
411 @register.simple_tag
412 def download_audio(book, daisy=True):
413     links = []
414     if book.has_media('mp3'):
415         links.append("<a href='%s'>%s</a>" % (
416             reverse('download_zip_mp3', args=[book.slug]), BookMedia.formats['mp3'].name))
417     if book.has_media('ogg'):
418         links.append("<a href='%s'>%s</a>" % (
419             reverse('download_zip_ogg', args=[book.slug]), BookMedia.formats['ogg'].name))
420     if daisy and book.has_media('daisy'):
421         for dsy in book.get_media('daisy'):
422             links.append("<a href='%s'>%s</a>" % (dsy.file.url, BookMedia.formats['daisy'].name))
423     return "".join(links)
424
425
426 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
427 def custom_pdf_link_li(book):
428     return {
429         'book': book,
430         'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
431     }
432
433
434 @register.inclusion_tag("catalogue/snippets/license_icon.html")
435 def license_icon(license_url):
436     """Creates a license icon, if the license_url is known."""
437     known = LICENSES.get(license_url)
438     if known is None:
439         return {}
440     return {
441         "license_url": license_url,
442         "icon": "img/licenses/%s.png" % known['icon'],
443         "license_description": known['description'],
444     }
445
446
447 @register.filter
448 def class_name(obj):
449     return obj.__class__.__name__
450
451
452 @register.simple_tag
453 def source_name(url):
454     url = url.lstrip()
455     netloc = urlparse(url).netloc
456     if not netloc:
457         netloc = urlparse('http://' + url).netloc
458     if not netloc:
459         return ''
460     source, created = Source.objects.get_or_create(netloc=netloc)
461     return source.name or netloc
462
463
464 @ssi_variable(register, patch_response=[add_never_cache_headers])
465 def catalogue_random_book(request, exclude_ids):
466     from .. import app_settings
467     if random() < app_settings.RELATED_RANDOM_PICTURE_CHANCE:
468         return None
469     queryset = Book.objects.exclude(pk__in=exclude_ids)
470     count = queryset.count()
471     if count:
472         return queryset[randint(0, count - 1)].pk
473     else:
474         return None
475
476
477 @ssi_variable(register, patch_response=[add_never_cache_headers])
478 def choose_fragment(request, book_id=None, tag_ids=None, unless=False):
479     if unless:
480         return None
481
482     if book_id is not None:
483         fragment = Book.objects.get(pk=book_id).choose_fragment()
484     else:
485         if tag_ids is not None:
486             tags = Tag.objects.filter(pk__in=tag_ids)
487             fragments = Fragment.tagged.with_all(tags).order_by().only('id')
488         else:
489             fragments = Fragment.objects.all().order_by().only('id')
490         fragment_count = fragments.count()
491         fragment = fragments[randint(0, fragment_count - 1)] if fragment_count else None
492     return fragment.pk if fragment is not None else None