Dynamic menu
[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
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/book_info.html')
315 def book_info(book):
316     return locals()
317
318
319 @register.inclusion_tag('catalogue/book_wide.html', takes_context=True)
320 def book_wide(context, book):
321     book_themes = book.related_themes()
322     extra_info = book.extra_info
323     hide_about = extra_info.get('about', '').startswith('http://wiki.wolnepodreczniki.pl')
324
325     return {
326         'book': book,
327         'main_link': reverse('book_text', args=[book.slug]) if book.html_file else None,
328         'related': book.related_info(),
329         'extra_info': extra_info,
330         'hide_about': hide_about,
331         'themes': book_themes,
332         'request': context.get('request'),
333     }
334
335
336 @register.inclusion_tag('catalogue/book_short.html', takes_context=True)
337 def book_short(context, book):
338     return {
339         'book': book,
340         'main_link': book.get_absolute_url(),
341         'related': book.related_info(),
342         'request': context.get('request'),
343     }
344
345
346 @register.inclusion_tag('catalogue/book_mini_box.html')
347 def book_mini(book):
348     return {
349         'book': book,
350         'related': book.related_info(),
351     }
352
353
354 @register.inclusion_tag('catalogue/work-list.html', takes_context=True)
355 def work_list(context, object_list):
356     request = context.get('request')
357     if object_list:
358         object_type = type(object_list[0]).__name__
359     return locals()
360
361
362 @register.inclusion_tag('catalogue/fragment_promo.html')
363 def fragment_promo(arg=None):
364     if arg is None:
365         fragments = Fragment.objects.all().order_by('?')
366         fragment = fragments[0] if fragments.exists() else None
367     elif isinstance(arg, Book):
368         fragment = arg.choose_fragment()
369     else:
370         fragments = Fragment.tagged.with_all(arg).order_by('?')
371         fragment = fragments[0] if fragments.exists() else None
372
373     return {
374         'fragment': fragment,
375     }
376
377
378 @register.inclusion_tag('catalogue/related_books.html')
379 def related_books(book, limit=6, random=1):
380     cache_key = "catalogue.related_books.%d.%d" % (book.id, limit - random)
381     related = cache.get(cache_key)
382     if related is None:
383         related = list(Book.objects.filter(
384             common_slug=book.common_slug).exclude(pk=book.pk)[:limit])
385         limit -= len(related)
386         if limit > random:
387             related += Book.tagged.related_to(book,
388                     Book.objects.exclude(common_slug=book.common_slug),
389                     ignore_by_tag=book.book_tag())[:limit-random]
390         cache.set(cache_key, related, 1800)
391     if random:
392         random_books = Book.objects.exclude(
393                         pk__in=[b.pk for b in related] + [book.pk])
394         if random == 1:
395             count = random_books.count()
396             if count:
397                 related.append(random_books[randint(0, count - 1)])
398         else:
399             related += list(random_books.order_by('?')[:random])
400     return {
401         'books': related,
402     }
403
404
405 @register.inclusion_tag('catalogue/menu.html')
406 def catalogue_menu():
407     return {'categories': [
408                 ('author', _('Authors'), 'autorzy'),
409                 ('genre', _('Genres'), 'gatunki'),
410                 ('kind', _('Kinds'), 'rodzaje'),
411                 ('epoch', _('Epochs'), 'epoki'),
412                 ('theme', _('Themes'), 'autorzy'),
413         ]}
414
415
416 @register.simple_tag
417 def tag_url(category, slug):
418     return Tag.create_url(category, slug)
419
420
421 @register.simple_tag
422 def download_audio(book, daisy=True):
423     related = book.related_info()
424     links = []
425     if related['media'].get('mp3'):
426         links.append("<a href='%s'>%s</a>" %
427             (reverse('download_zip_mp3', args=[book.slug]),
428                 BookMedia.formats['mp3'].name))
429     if related['media'].get('ogg'):
430         links.append("<a href='%s'>%s</a>" %
431             (reverse('download_zip_ogg', args=[book.slug]),
432                 BookMedia.formats['ogg'].name))
433     if daisy and related['media'].get('daisy'):
434         for dsy in book.get_media('daisy'):
435             links.append("<a href='%s'>%s</a>" %
436                 (dsy.file.url, BookMedia.formats['daisy'].name))
437     return ", ".join(links)
438
439
440 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
441 def custom_pdf_link_li(book):
442     return {
443         'book': book,
444         'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
445     }
446
447
448 @register.inclusion_tag("catalogue/snippets/license_icon.html")
449 def license_icon(license_url):
450     """Creates a license icon, if the license_url is known."""
451     known = LICENSES.get(license_url)
452     if known is None:
453         return {}
454     return {
455         "license_url": license_url,
456         "icon": "img/licenses/%s.png" % known['icon'],
457         "license_description": known['description'],
458     }