fd8c8c4b9ba0b7d5967021da3d846fc031e511a8
[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 from urlparse import urlparse
9
10 from django.conf import settings
11 from django import template
12 from django.template import Node, Variable, Template, Context
13 from django.core.cache import cache
14 from django.core.urlresolvers import reverse
15 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
16 from django.utils.translation import ugettext as _
17
18 from catalogue.utils import related_tag_name as _related_tag_name
19 from catalogue.models import Book, BookMedia, Fragment, Tag, Source
20 from catalogue.constants import LICENSES
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 class CatalogueURLNode(Node):
247     def __init__(self, tags_to_add, tags_to_remove):
248         self.tags_to_add = [Variable(tag) for tag in tags_to_add]
249         self.tags_to_remove = [Variable(tag) for tag in tags_to_remove]
250
251     def render(self, context):
252         tags_to_add = []
253         tags_to_remove = []
254
255         for tag_variable in self.tags_to_add:
256             tag = tag_variable.resolve(context)
257             if isinstance(tag, (list, dict)):
258                 tags_to_add += [t for t in tag]
259             else:
260                 tags_to_add.append(tag)
261
262         for tag_variable in self.tags_to_remove:
263             tag = tag_variable.resolve(context)
264             if iterable(tag):
265                 tags_to_remove += [t for t in tag]
266             else:
267                 tags_to_remove.append(tag)
268
269         tag_slugs = [tag.url_chunk for tag in tags_to_add]
270         for tag in tags_to_remove:
271             try:
272                 tag_slugs.remove(tag.url_chunk)
273             except KeyError:
274                 pass
275
276         if len(tag_slugs) > 0:
277             return reverse('tagged_object_list', kwargs={'tags': '/'.join(tag_slugs)})
278         else:
279             return reverse('main_page')
280
281
282 @register.inclusion_tag('catalogue/latest_blog_posts.html')
283 def latest_blog_posts(feed_url, posts_to_show=5):
284     try:
285         feed = feedparser.parse(str(feed_url))
286         posts = []
287         for i in range(posts_to_show):
288             pub_date = feed['entries'][i].published_parsed
289             published = datetime.date(pub_date[0], pub_date[1], pub_date[2])
290             posts.append({
291                 'title': feed['entries'][i].title,
292                 'summary': feed['entries'][i].summary,
293                 'link': feed['entries'][i].link,
294                 'date': published,
295                 })
296         return {'posts': posts}
297     except:
298         return {'posts': []}
299
300
301 @register.inclusion_tag('catalogue/tag_list.html')
302 def tag_list(tags, choices=None):
303     if choices is None:
304         choices = []
305     if len(tags) == 1 and tags[0].category not in [t.category for t in choices]:
306         one_tag = tags[0]
307     return locals()
308
309
310 @register.inclusion_tag('catalogue/inline_tag_list.html')
311 def inline_tag_list(tags, choices=None):
312     return tag_list(tags, choices)
313
314
315 @register.inclusion_tag('catalogue/collection_list.html')
316 def collection_list(collections):
317     return locals()
318
319
320 @register.inclusion_tag('catalogue/book_info.html')
321 def book_info(book):
322     return locals()
323
324
325 @register.inclusion_tag('catalogue/book_wide.html', takes_context=True)
326 def book_wide(context, book):
327     book_themes = book.related_themes()
328     extra_info = book.extra_info
329     hide_about = extra_info.get('about', '').startswith('http://wiki.wolnepodreczniki.pl')
330     stage_note, stage_note_url = book.stage_note()
331
332     return {
333         'book': book,
334         'main_link': reverse('book_text', args=[book.slug]) if book.html_file else None,
335         'related': book.related_info(),
336         'extra_info': extra_info,
337         'hide_about': hide_about,
338         'themes': book_themes,
339         'request': context.get('request'),
340         'show_lang': book.language_code() != settings.LANGUAGE_CODE,
341         'stage_note': stage_note,
342         'stage_note_url': stage_note_url,
343     }
344
345
346 @register.inclusion_tag('catalogue/book_short.html', takes_context=True)
347 def book_short(context, book):
348     stage_note, stage_note_url = book.stage_note()
349
350     return {
351         'book': book,
352         'main_link': book.get_absolute_url(),
353         'related': book.related_info(),
354         'request': context.get('request'),
355         'show_lang': book.language_code() != settings.LANGUAGE_CODE,
356         'stage_note': stage_note,
357         'stage_note_url': stage_note_url,
358     }
359
360
361 @register.inclusion_tag('catalogue/book_mini_box.html')
362 def book_mini(book, with_link=True):
363     author_str = ", ".join(related_tag_name(tag)
364         for tag in book.related_info()['tags'].get('author', ()))
365     return {
366         'book': book,
367         'author_str': author_str,
368         'with_link': with_link,
369         'show_lang': book.language_code() != settings.LANGUAGE_CODE,
370     }
371
372
373 @register.inclusion_tag('catalogue/work-list.html', takes_context=True)
374 def work_list(context, object_list):
375     request = context.get('request')
376     return locals()
377
378
379 @register.inclusion_tag('catalogue/fragment_promo.html')
380 def fragment_promo(arg=None):
381     if arg is None:
382         fragments = Fragment.objects.all().order_by('?')
383         fragment = fragments[0] if fragments.exists() else None
384     elif isinstance(arg, Book):
385         fragment = arg.choose_fragment()
386     else:
387         fragments = Fragment.tagged.with_all(arg).order_by('?')
388         fragment = fragments[0] if fragments.exists() else None
389
390     return {
391         'fragment': fragment,
392     }
393
394
395 @register.inclusion_tag('catalogue/related_books.html')
396 def related_books(book, limit=6, random=1, taken=0):
397     limit = limit - taken
398     cache_key = "catalogue.related_books.%d.%d" % (book.id, limit - random)
399     related = cache.get(cache_key)
400     if related is None:
401         related = Book.tagged.related_to(book,
402                 Book.objects.exclude(common_slug=book.common_slug),
403                 ignore_by_tag=book.book_tag())[:limit-random]
404         cache.set(cache_key, related, 1800)
405     if random:
406         random_books = Book.objects.exclude(
407                         pk__in=[b.pk for b in related] + [book.pk])
408         if random == 1:
409             count = random_books.count()
410             if count:
411                 related.append(random_books[randint(0, count - 1)])
412         else:
413             related += list(random_books.order_by('?')[:random])
414     return {
415         'books': related,
416     }
417
418
419 @register.inclusion_tag('catalogue/menu.html')
420 def catalogue_menu():
421     return {'categories': [
422                 ('author', _('Authors'), 'autorzy'),
423                 ('genre', _('Genres'), 'gatunki'),
424                 ('kind', _('Kinds'), 'rodzaje'),
425                 ('epoch', _('Epochs'), 'epoki'),
426                 ('theme', _('Themes'), 'motywy'),
427         ]}
428
429
430 @register.simple_tag
431 def tag_url(category, slug):
432     return Tag.create_url(category, slug)
433
434
435 @register.simple_tag
436 def download_audio(book, daisy=True):
437     related = book.related_info()
438     links = []
439     if related['media'].get('mp3'):
440         links.append("<a href='%s'>%s</a>" %
441             (reverse('download_zip_mp3', args=[book.slug]),
442                 BookMedia.formats['mp3'].name))
443     if related['media'].get('ogg'):
444         links.append("<a href='%s'>%s</a>" %
445             (reverse('download_zip_ogg', args=[book.slug]),
446                 BookMedia.formats['ogg'].name))
447     if daisy and related['media'].get('daisy'):
448         for dsy in book.get_media('daisy'):
449             links.append("<a href='%s'>%s</a>" %
450                 (dsy.file.url, BookMedia.formats['daisy'].name))
451     return ", ".join(links)
452
453
454 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
455 def custom_pdf_link_li(book):
456     return {
457         'book': book,
458         'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
459     }
460
461
462 @register.inclusion_tag("catalogue/snippets/license_icon.html")
463 def license_icon(license_url):
464     """Creates a license icon, if the license_url is known."""
465     known = LICENSES.get(license_url)
466     if known is None:
467         return {}
468     return {
469         "license_url": license_url,
470         "icon": "img/licenses/%s.png" % known['icon'],
471         "license_description": known['description'],
472     }
473
474
475 @register.simple_tag
476 def related_tag_name(tag, lang=None):
477     return _related_tag_name(tag, lang)
478
479
480 @register.filter
481 def class_name(obj):
482     return obj.__class__.__name__
483
484
485 @register.simple_tag
486 def source_name(url):
487     url = url.lstrip()
488     netloc = urlparse(url).netloc
489     if not netloc:
490         netloc = urlparse('http://' + url).netloc
491     if not netloc:
492         return ''
493     source, created = Source.objects.get_or_create(netloc=netloc)
494     return source.name or netloc