Merge branch 'master' into obrazy
[wolnelektury.git] / apps / 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 import datetime
6 import feedparser
7 from random import randint
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.cache import cache
13 from django.core.urlresolvers import reverse
14 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
15 from django.utils.translation import ugettext as _
16
17 from catalogue.utils import split_tags, related_tag_name as _related_tag_name
18 from catalogue.models import Book, BookMedia, Fragment, Tag
19 from catalogue.constants import LICENSES
20
21 register = template.Library()
22
23
24 class RegistrationForm(UserCreationForm):
25     def as_ul(self):
26         "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
27         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)
28
29
30 class LoginForm(AuthenticationForm):
31     def as_ul(self):
32         "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
33         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)
34
35
36 def iterable(obj):
37     try:
38         iter(obj)
39         return True
40     except TypeError:
41         return False
42
43
44 def capfirst(text):
45     try:
46         return '%s%s' % (text[0].upper(), text[1:])
47     except IndexError:
48         return ''
49
50
51 @register.simple_tag
52 def html_title_from_tags(tags):
53     if len(tags) < 2:
54         return title_from_tags(tags)
55     template = Template("{{ category }}: <a href='{{ tag.get_absolute_url }}'>{{ tag.name }}</a>")
56     return capfirst(",<br/>".join(
57         template.render(Context({'tag': tag, 'category': _(tag.category)})) for tag in tags))
58     
59
60 def simple_title(tags):
61     title = []
62     for tag in tags:
63         title.append("%s: %s" % (_(tag.category), tag.name))
64     return capfirst(', '.join(title))
65
66
67 @register.simple_tag
68 def book_title(book, html_links=False):
69     return book.pretty_title(html_links)
70
71
72 @register.simple_tag
73 def book_title_html(book):
74     return book_title(book, html_links=True)
75
76
77 @register.simple_tag
78 def title_from_tags(tags):
79     def split_tags(tags):
80         result = {}
81         for tag in tags:
82             result[tag.category] = tag
83         return result
84
85     # TODO: Remove this after adding flection mechanism
86     return simple_title(tags)
87
88     class Flection(object):
89         def get_case(self, name, flection):
90             return name
91     flection = Flection()
92
93     self = split_tags(tags)
94
95     title = u''
96
97     # Specjalny przypadek oglądania wszystkich lektur na danej półce
98     if len(self) == 1 and 'set' in self:
99         return u'Półka %s' % self['set']
100
101     # Specjalny przypadek "Twórczość w pozytywizmie", wtedy gdy tylko epoka
102     # jest wybrana przez użytkownika
103     if 'epoch' in self and len(self) == 1:
104         text = u'Twórczość w %s' % flection.get_case(unicode(self['epoch']), u'miejscownik')
105         return capfirst(text)
106
107     # Specjalny przypadek "Dramat w twórczości Sofoklesa", wtedy gdy podane
108     # są tylko rodzaj literacki i autor
109     if 'kind' in self and 'author' in self and len(self) == 2:
110         text = u'%s w twórczości %s' % (unicode(self['kind']),
111             flection.get_case(unicode(self['author']), u'dopełniacz'))
112         return capfirst(text)
113
114     # Przypadki ogólniejsze
115     if 'theme' in self:
116         title += u'Motyw %s' % unicode(self['theme'])
117
118     if 'genre' in self:
119         if 'theme' in self:
120             title += u' w %s' % flection.get_case(unicode(self['genre']), u'miejscownik')
121         else:
122             title += unicode(self['genre'])
123
124     if 'kind' in self or 'author' in self or 'epoch' in self:
125         if 'genre' in self or 'theme' in self:
126             if 'kind' in self:
127                 title += u' w %s ' % flection.get_case(unicode(self['kind']), u'miejscownik')
128             else:
129                 title += u' w twórczości '
130         else:
131             title += u'%s ' % unicode(self.get('kind', u'twórczość'))
132
133     if 'author' in self:
134         title += flection.get_case(unicode(self['author']), u'dopełniacz')
135     elif 'epoch' in self:
136         title += flection.get_case(unicode(self['epoch']), u'dopełniacz')
137
138     return capfirst(title)
139
140
141 @register.simple_tag
142 def book_tree(book_list, books_by_parent):
143     text = "".join("<li><a href='%s'>%s</a>%s</li>" % (
144         book.get_absolute_url(), book.title, book_tree(books_by_parent.get(book, ()), books_by_parent)
145         ) for book in book_list)
146
147     if text:
148         return "<ol>%s</ol>" % text
149     else:
150         return ''
151
152 @register.simple_tag
153 def audiobook_tree(book_list, books_by_parent):
154     text = "".join("<li><a class='open-player' href='%s'>%s</a>%s</li>" % (
155         reverse("book_player", args=[book.slug]), book.title, audiobook_tree(books_by_parent.get(book, ()), books_by_parent)
156         ) for book in book_list)
157
158     if text:
159         return "<ol>%s</ol>" % text
160     else:
161         return ''
162
163 @register.simple_tag
164 def book_tree_texml(book_list, books_by_parent, depth=1):
165     return "".join("""
166             <cmd name='hspace'><parm>%(depth)dem</parm></cmd>%(title)s
167             <spec cat='align' /><cmd name="note"><parm>%(audiences)s</parm></cmd>
168             <spec cat='align' /><cmd name="note"><parm>%(audiobook)s</parm></cmd>
169             <ctrl ch='\\' />
170             %(children)s
171             """ % {
172                 "depth": depth,
173                 "title": book.title,
174                 "audiences": ", ".join(book.audiences_pl()),
175                 "audiobook": "audiobook" if book.has_media('mp3') else "",
176                 "children": book_tree_texml(books_by_parent.get(book.id, ()), books_by_parent, depth + 1)
177             } for book in book_list)
178
179
180 @register.simple_tag
181 def book_tree_csv(author, book_list, books_by_parent, depth=1, max_depth=3, delimeter="\t"):
182     def quote_if_necessary(s):
183         try:
184             s.index(delimeter)
185             s.replace('"', '\\"')
186             return '"%s"' % s
187         except ValueError:
188             return s
189         
190     return "".join("""%(author)s%(d)s%(preindent)s%(title)s%(d)s%(postindent)s%(audiences)s%(d)s%(audiobook)s
191 %(children)s""" % {
192                 "d": delimeter,
193                 "preindent": delimeter * (depth - 1),
194                 "postindent": delimeter * (max_depth - depth), 
195                 "depth": depth,
196                 "author": quote_if_necessary(author.name),
197                 "title": quote_if_necessary(book.title),
198                 "audiences": ", ".join(book.audiences_pl()),
199                 "audiobook": "audiobook" if book.has_media('mp3') else "",
200                 "children": book_tree_csv(author, books_by_parent.get(book.id, ()), books_by_parent, depth + 1)
201             } for book in book_list)
202
203 @register.simple_tag
204 def all_editors(extra_info):
205     editors = []
206     if 'editors' in extra_info:
207         editors += extra_info['editors']
208     if 'technical_editors' in extra_info:
209         editors += extra_info['technical_editors']
210     # support for extra_info-s from librarian<1.2
211     if 'editor' in extra_info:
212         editors.append(extra_info['editor'])
213     if 'technical_editor' in extra_info:
214         editors.append(extra_info['technical_editor'])
215     return ', '.join(
216                      ' '.join(p.strip() for p in person.rsplit(',', 1)[::-1])
217                      for person in sorted(set(editors)))
218
219
220 @register.simple_tag
221 def user_creation_form():
222     return RegistrationForm(prefix='registration').as_ul()
223
224
225 @register.simple_tag
226 def authentication_form():
227     return LoginForm(prefix='login').as_ul()
228
229
230 @register.tag
231 def catalogue_url(parser, token):
232     bits = token.split_contents()
233
234     tags_to_add = []
235     tags_to_remove = []
236     for bit in bits[1:]:
237         if bit[0] == '-':
238             tags_to_remove.append(bit[1:])
239         else:
240             tags_to_add.append(bit)
241
242     return CatalogueURLNode(tags_to_add, tags_to_remove)
243
244
245 class CatalogueURLNode(Node):
246     def __init__(self, tags_to_add, tags_to_remove):
247         self.tags_to_add = [Variable(tag) for tag in tags_to_add]
248         self.tags_to_remove = [Variable(tag) for tag in tags_to_remove]
249
250     def render(self, context):
251         tags_to_add = []
252         tags_to_remove = []
253
254         for tag_variable in self.tags_to_add:
255             tag = tag_variable.resolve(context)
256             if isinstance(tag, (list, dict)):
257                 tags_to_add += [t for t in tag]
258             else:
259                 tags_to_add.append(tag)
260
261         for tag_variable in self.tags_to_remove:
262             tag = tag_variable.resolve(context)
263             if iterable(tag):
264                 tags_to_remove += [t for t in tag]
265             else:
266                 tags_to_remove.append(tag)
267
268         tag_slugs = [tag.url_chunk for tag in tags_to_add]
269         for tag in tags_to_remove:
270             try:
271                 tag_slugs.remove(tag.url_chunk)
272             except KeyError:
273                 pass
274
275         if len(tag_slugs) > 0:
276             return reverse('tagged_object_list', kwargs={'tags': '/'.join(tag_slugs)})
277         else:
278             return reverse('main_page')
279
280
281 @register.inclusion_tag('catalogue/latest_blog_posts.html')
282 def latest_blog_posts(feed_url, posts_to_show=5):
283     try:
284         feed = feedparser.parse(str(feed_url))
285         posts = []
286         for i in range(posts_to_show):
287             pub_date = feed['entries'][i].updated_parsed
288             published = datetime.date(pub_date[0], pub_date[1], pub_date[2] )
289             posts.append({
290                 'title': feed['entries'][i].title,
291                 'summary': feed['entries'][i].summary,
292                 'link': feed['entries'][i].link,
293                 'date': published,
294                 })
295         return {'posts': posts}
296     except:
297         return {'posts': []}
298
299
300 @register.inclusion_tag('catalogue/tag_list.html')
301 def tag_list(tags, choices=None):
302     if choices is None:
303         choices = []
304     if len(tags) == 1 and tags[0].category not in [t.category for t in choices]:
305         one_tag = tags[0]
306     return locals()
307
308
309 @register.inclusion_tag('catalogue/inline_tag_list.html')
310 def inline_tag_list(tags, choices=None):
311     return tag_list(tags, choices)
312
313
314 @register.inclusion_tag('catalogue/collection_list.html')
315 def collection_list(collections):
316     return locals()
317
318
319 @register.inclusion_tag('catalogue/book_info.html')
320 def book_info(book):
321     return locals()
322
323
324 @register.inclusion_tag('catalogue/book_wide.html', takes_context=True)
325 def book_wide(context, book):
326     book_themes = book.related_themes()
327     extra_info = book.extra_info
328     hide_about = extra_info.get('about', '').startswith('http://wiki.wolnepodreczniki.pl')
329
330     return {
331         'book': book,
332         'main_link': reverse('book_text', args=[book.slug]) if book.html_file else None,
333         'related': book.related_info(),
334         'extra_info': extra_info,
335         'hide_about': hide_about,
336         'themes': book_themes,
337         'request': context.get('request'),
338     }
339
340
341 @register.inclusion_tag('catalogue/book_short.html', takes_context=True)
342 def book_short(context, book):
343     return {
344         'book': book,
345         'main_link': book.get_absolute_url(),
346         'related': book.related_info(),
347         'request': context.get('request'),
348     }
349
350
351 @register.inclusion_tag('catalogue/book_mini_box.html')
352 def book_mini(book):
353     author_str = ", ".join(related_tag_name(tag)
354         for tag in book.related_info()['tags'].get('author', ()))
355     return {
356         'book': book,
357         'author_str': author_str,
358     }
359
360
361 @register.inclusion_tag('catalogue/work-list.html', takes_context=True)
362 def work_list(context, object_list):
363     request = context.get('request')
364     if object_list:
365         object_type = type(object_list[0]).__name__
366     return locals()
367
368
369 @register.inclusion_tag('catalogue/fragment_promo.html')
370 def fragment_promo(arg=None):
371     if arg is None:
372         fragments = Fragment.objects.all().order_by('?')
373         fragment = fragments[0] if fragments.exists() else None
374     elif isinstance(arg, Book):
375         fragment = arg.choose_fragment()
376     else:
377         fragments = Fragment.tagged.with_all(arg).order_by('?')
378         fragment = fragments[0] if fragments.exists() else None
379
380     return {
381         'fragment': fragment,
382     }
383
384
385 @register.inclusion_tag('catalogue/related_books.html')
386 def related_books(book, limit=6, random=1):
387     cache_key = "catalogue.related_books.%d.%d" % (book.id, limit - random)
388     related = cache.get(cache_key)
389     if related is None:
390         related = list(Book.objects.filter(
391             common_slug=book.common_slug).exclude(pk=book.pk)[:limit])
392         limit -= len(related)
393         if limit > random:
394             related += Book.tagged.related_to(book,
395                     Book.objects.exclude(common_slug=book.common_slug),
396                     ignore_by_tag=book.book_tag())[:limit-random]
397         cache.set(cache_key, related, 1800)
398     if random:
399         random_books = Book.objects.exclude(
400                         pk__in=[b.pk for b in related] + [book.pk])
401         if random == 1:
402             count = random_books.count()
403             if count:
404                 related.append(random_books[randint(0, count - 1)])
405         else:
406             related += list(random_books.order_by('?')[:random])
407     return {
408         'books': related,
409     }
410
411
412 @register.inclusion_tag('catalogue/menu.html')
413 def catalogue_menu():
414     return {'categories': [
415                 ('author', _('Authors'), 'autorzy'),
416                 ('genre', _('Genres'), 'gatunki'),
417                 ('kind', _('Kinds'), 'rodzaje'),
418                 ('epoch', _('Epochs'), 'epoki'),
419                 ('theme', _('Themes'), 'autorzy'),
420         ]}
421
422
423 @register.simple_tag
424 def tag_url(category, slug):
425     return Tag.create_url(category, slug)
426
427
428 @register.simple_tag
429 def download_audio(book, daisy=True):
430     related = book.related_info()
431     links = []
432     if related['media'].get('mp3'):
433         links.append("<a href='%s'>%s</a>" %
434             (reverse('download_zip_mp3', args=[book.slug]),
435                 BookMedia.formats['mp3'].name))
436     if related['media'].get('ogg'):
437         links.append("<a href='%s'>%s</a>" %
438             (reverse('download_zip_ogg', args=[book.slug]),
439                 BookMedia.formats['ogg'].name))
440     if daisy and related['media'].get('daisy'):
441         for dsy in book.get_media('daisy'):
442             links.append("<a href='%s'>%s</a>" %
443                 (dsy.file.url, BookMedia.formats['daisy'].name))
444     return ", ".join(links)
445
446
447 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
448 def custom_pdf_link_li(book):
449     return {
450         'book': book,
451         'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
452     }
453
454
455 @register.inclusion_tag("catalogue/snippets/license_icon.html")
456 def license_icon(license_url):
457     """Creates a license icon, if the license_url is known."""
458     known = LICENSES.get(license_url)
459     if known is None:
460         return {}
461     return {
462         "license_url": license_url,
463         "icon": "img/licenses/%s.png" % known['icon'],
464         "license_description": known['description'],
465     }
466
467
468 @register.simple_tag
469 def related_tag_name(tag, lang=None):
470     return _related_tag_name(tag, lang)