Some cleaning.
[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 split_tags
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     ctx = book_short(context, book)
328     ctx['extra_info'] = book.extra_info
329     ctx['hide_about'] = ctx['extra_info'].get('about', '').startswith('http://wiki.wolnepodreczniki.pl')
330     ctx['themes'] = book.related_themes()
331     ctx['main_link'] = reverse('book_text', args=[book.slug]) if book.html_file else None
332     return ctx
333
334
335 @register.inclusion_tag('catalogue/book_short.html', takes_context=True)
336 def book_short(context, book):
337     stage_note, stage_note_url = book.stage_note()
338
339     return {
340         'book': book,
341         'has_audio': book.has_media('mp3'),
342         'main_link': book.get_absolute_url(),
343         'parents': book.parents(),
344         'tags': split_tags(book.tags.exclude(category__in=('set', 'theme'))),
345         'request': context.get('request'),
346         'show_lang': book.language_code() != settings.LANGUAGE_CODE,
347         'stage_note': stage_note,
348         'stage_note_url': stage_note_url,
349     }
350
351
352 @register.inclusion_tag('catalogue/book_mini_box.html')
353 def book_mini(book, with_link=True):
354     author_str = ", ".join(tag.name
355         for tag in book.tags.filter(category='author'))
356     return {
357         'book': book,
358         'author_str': author_str,
359         'with_link': with_link,
360         'show_lang': book.language_code() != settings.LANGUAGE_CODE,
361     }
362
363
364 @register.inclusion_tag('catalogue/work-list.html', takes_context=True)
365 def work_list(context, object_list):
366     request = context.get('request')
367     return locals()
368
369
370 @register.inclusion_tag('catalogue/fragment_promo.html')
371 def fragment_promo(arg=None):
372     if isinstance(arg, Book):
373         fragment = arg.choose_fragment()
374     else:
375         if arg is None:
376             fragments = Fragment.objects.all()
377         else:
378             fragments = Fragment.tagged.with_all(arg)
379         fragments = fragments.order_by().only('id')
380         fragments_count = fragments.count()
381         if fragments_count:
382             fragment = fragments.order_by()[randint(0, fragments_count - 1)]
383         else:
384             fragment = None
385
386     return {
387         'fragment': fragment,
388     }
389
390
391 @register.inclusion_tag('catalogue/related_books.html')
392 def related_books(book, limit=6, random=1, taken=0):
393     limit = limit - taken
394     cache_key = "catalogue.related_books.%d.%d" % (book.id, limit - random)
395     related = cache.get(cache_key)
396     if related is None:
397         related = Book.tagged.related_to(book,
398                 Book.objects.exclude(common_slug=book.common_slug)
399                 ).exclude(ancestor=book)[:limit-random]
400         cache.set(cache_key, related, 1800)
401     if random:
402         random_books = Book.objects.exclude(
403                         pk__in=[b.pk for b in related] + [book.pk])
404         if random == 1:
405             count = random_books.count()
406             if count:
407                 random_related = [random_books[randint(0, count - 1)]]
408         else:
409             random_related = list(random_books.order_by('?')[:random])
410     else:
411         random_related = []
412     return {
413         'books': related,
414         'random_related': random_related,
415     }
416
417
418 @register.inclusion_tag('catalogue/menu.html')
419 def catalogue_menu():
420     return {'categories': [
421                 ('author', _('Authors'), 'autorzy'),
422                 ('genre', _('Genres'), 'gatunki'),
423                 ('kind', _('Kinds'), 'rodzaje'),
424                 ('epoch', _('Epochs'), 'epoki'),
425                 ('theme', _('Themes'), 'motywy'),
426         ]}
427
428
429 @register.simple_tag
430 def tag_url(category, slug):
431     return Tag.create_url(category, slug)
432
433
434 @register.simple_tag
435 def download_audio(book, daisy=True):
436     links = []
437     if book.has_media('mp3'):
438         links.append("<a href='%s'>%s</a>" %
439             (reverse('download_zip_mp3', args=[book.slug]),
440                 BookMedia.formats['mp3'].name))
441     if book.has_media('ogg'):
442         links.append("<a href='%s'>%s</a>" %
443             (reverse('download_zip_ogg', args=[book.slug]),
444                 BookMedia.formats['ogg'].name))
445     if daisy and book.has_media('daisy'):
446         for dsy in book.get_media('daisy'):
447             links.append("<a href='%s'>%s</a>" %
448                 (dsy.file.url, BookMedia.formats['daisy'].name))
449     return ", ".join(links)
450
451
452 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
453 def custom_pdf_link_li(book):
454     return {
455         'book': book,
456         'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
457     }
458
459
460 @register.inclusion_tag("catalogue/snippets/license_icon.html")
461 def license_icon(license_url):
462     """Creates a license icon, if the license_url is known."""
463     known = LICENSES.get(license_url)
464     if known is None:
465         return {}
466     return {
467         "license_url": license_url,
468         "icon": "img/licenses/%s.png" % known['icon'],
469         "license_description": known['description'],
470     }
471
472
473 @register.filter
474 def class_name(obj):
475     return obj.__class__.__name__
476
477
478 @register.simple_tag
479 def source_name(url):
480     url = url.lstrip()
481     netloc = urlparse(url).netloc
482     if not netloc:
483         netloc = urlparse('http://' + url).netloc
484     if not netloc:
485         return ''
486     source, created = Source.objects.get_or_create(netloc=netloc)
487     return source.name or netloc