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