Ebooks are now named after the book's slug and overwrite old versions.
[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 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].published_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         'show_lang': book.language_code() != settings.LANGUAGE_CODE,
339     }
340
341
342 @register.inclusion_tag('catalogue/book_short.html', takes_context=True)
343 def book_short(context, book):
344     return {
345         'book': book,
346         'main_link': book.get_absolute_url(),
347         'related': book.related_info(),
348         'request': context.get('request'),
349         'show_lang': book.language_code() != settings.LANGUAGE_CODE,
350     }
351
352
353 @register.inclusion_tag('catalogue/book_mini_box.html')
354 def book_mini(book, with_link=True):
355     author_str = ", ".join(related_tag_name(tag)
356         for tag in book.related_info()['tags'].get('author', ()))
357     return {
358         'book': book,
359         'author_str': author_str,
360         'with_link': with_link,
361         'show_lang': book.language_code() != settings.LANGUAGE_CODE,
362     }
363
364
365 @register.inclusion_tag('catalogue/work-list.html', takes_context=True)
366 def work_list(context, object_list):
367     request = context.get('request')
368     for obj in object_list:
369         obj.object_type = type(obj).__name__
370
371     return locals()
372
373
374 @register.inclusion_tag('catalogue/fragment_promo.html')
375 def fragment_promo(arg=None):
376     if arg is None:
377         fragments = Fragment.objects.all().order_by('?')
378         fragment = fragments[0] if fragments.exists() else None
379     elif isinstance(arg, Book):
380         fragment = arg.choose_fragment()
381     else:
382         fragments = Fragment.tagged.with_all(arg).order_by('?')
383         fragment = fragments[0] if fragments.exists() else None
384
385     return {
386         'fragment': fragment,
387     }
388
389
390 @register.inclusion_tag('catalogue/related_books.html')
391 def related_books(book, limit=6, random=1, taken=0):
392     limit = limit - taken
393     cache_key = "catalogue.related_books.%d.%d" % (book.id, limit - random)
394     related = cache.get(cache_key)
395     if related is None:
396         related = Book.tagged.related_to(book,
397                 Book.objects.exclude(common_slug=book.common_slug),
398                 ignore_by_tag=book.book_tag())[:limit-random]
399         cache.set(cache_key, related, 1800)
400     if random:
401         random_books = Book.objects.exclude(
402                         pk__in=[b.pk for b in related] + [book.pk])
403         if random == 1:
404             count = random_books.count()
405             if count:
406                 related.append(random_books[randint(0, count - 1)])
407         else:
408             related += list(random_books.order_by('?')[:random])
409     return {
410         'books': related,
411     }
412
413
414 @register.inclusion_tag('catalogue/menu.html')
415 def catalogue_menu():
416     return {'categories': [
417                 ('author', _('Authors'), 'autorzy'),
418                 ('genre', _('Genres'), 'gatunki'),
419                 ('kind', _('Kinds'), 'rodzaje'),
420                 ('epoch', _('Epochs'), 'epoki'),
421                 ('theme', _('Themes'), 'motywy'),
422         ]}
423
424
425 @register.simple_tag
426 def tag_url(category, slug):
427     return Tag.create_url(category, slug)
428
429
430 @register.simple_tag
431 def download_audio(book, daisy=True):
432     related = book.related_info()
433     links = []
434     if related['media'].get('mp3'):
435         links.append("<a href='%s'>%s</a>" %
436             (reverse('download_zip_mp3', args=[book.slug]),
437                 BookMedia.formats['mp3'].name))
438     if related['media'].get('ogg'):
439         links.append("<a href='%s'>%s</a>" %
440             (reverse('download_zip_ogg', args=[book.slug]),
441                 BookMedia.formats['ogg'].name))
442     if daisy and related['media'].get('daisy'):
443         for dsy in book.get_media('daisy'):
444             links.append("<a href='%s'>%s</a>" %
445                 (dsy.file.url, BookMedia.formats['daisy'].name))
446     return ", ".join(links)
447
448
449 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
450 def custom_pdf_link_li(book):
451     return {
452         'book': book,
453         'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
454     }
455
456
457 @register.inclusion_tag("catalogue/snippets/license_icon.html")
458 def license_icon(license_url):
459     """Creates a license icon, if the license_url is known."""
460     known = LICENSES.get(license_url)
461     if known is None:
462         return {}
463     return {
464         "license_url": license_url,
465         "icon": "img/licenses/%s.png" % known['icon'],
466         "license_description": known['description'],
467     }
468
469
470 @register.simple_tag
471 def related_tag_name(tag, lang=None):
472     return _related_tag_name(tag, lang)
473
474
475 @register.simple_tag
476 def class_name(obj):
477     return obj.__class__.__name__
478