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