#8 tagged object list for audiobooks
[wolnelektury.git] / src / 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 from random import randint, random
6 from urlparse import urlparse
7 from django.contrib.contenttypes.models import ContentType
8
9 from django.conf import settings
10 from django import template
11 from django.core.cache import cache
12 from django.template import Node, Variable, Template, Context
13 from django.core.urlresolvers import reverse
14 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
15 from django.utils.cache import add_never_cache_headers
16 from django.utils.translation import ugettext as _
17
18 from ssify import ssi_variable
19 from catalogue.models import Book, BookMedia, Fragment, Tag, Source
20 from catalogue.constants import LICENSES
21 from picture.models import Picture
22
23 register = template.Library()
24
25
26 class RegistrationForm(UserCreationForm):
27     def as_ul(self):
28         """Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."""
29         return self._html_output(
30             u'<li>%(errors)s%(label)s %(field)s<span class="help-text">%(help_text)s</span></li>', u'<li>%s</li>',
31             '</li>', u' %s', False)
32
33
34 class LoginForm(AuthenticationForm):
35     def as_ul(self):
36         """Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."""
37         return self._html_output(
38             u'<li>%(errors)s%(label)s %(field)s<span class="help-text">%(help_text)s</span></li>', u'<li>%s</li>',
39             '</li>', u' %s', False)
40
41
42 def iterable(obj):
43     try:
44         iter(obj)
45         return True
46     except TypeError:
47         return False
48
49
50 def capfirst(text):
51     try:
52         return '%s%s' % (text[0].upper(), text[1:])
53     except IndexError:
54         return ''
55
56
57 @register.simple_tag
58 def html_title_from_tags(tags):
59     if len(tags) < 2:
60         return title_from_tags(tags)
61     template = Template("{{ category }}: <a href='{{ tag.get_absolute_url }}'>{{ tag.name }}</a>")
62     return capfirst(",<br/>".join(
63         template.render(Context({'tag': tag, 'category': _(tag.category)})) for tag in tags))
64
65
66 def simple_title(tags):
67     title = []
68     for tag in tags:
69         title.append("%s: %s" % (_(tag.category), tag.name))
70     return capfirst(', '.join(title))
71
72
73 @register.simple_tag
74 def book_title(book, html_links=False):
75     return book.pretty_title(html_links)
76
77
78 @register.simple_tag
79 def book_title_html(book):
80     return book_title(book, html_links=True)
81
82
83 @register.simple_tag
84 def title_from_tags(tags):
85     def split_tags(tags):
86         result = {}
87         for tag in tags:
88             result[tag.category] = tag
89         return result
90
91     # TODO: Remove this after adding flection mechanism
92     return simple_title(tags)
93
94     class Flection(object):
95         def get_case(self, name, flection):
96             return name
97     flection = Flection()
98
99     self = split_tags(tags)
100
101     title = u''
102
103     # Specjalny przypadek oglądania wszystkich lektur na danej półce
104     if len(self) == 1 and 'set' in self:
105         return u'Półka %s' % self['set']
106
107     # Specjalny przypadek "Twórczość w pozytywizmie", wtedy gdy tylko epoka
108     # jest wybrana przez użytkownika
109     if 'epoch' in self and len(self) == 1:
110         text = u'Twórczość w %s' % flection.get_case(unicode(self['epoch']), u'miejscownik')
111         return capfirst(text)
112
113     # Specjalny przypadek "Dramat w twórczości Sofoklesa", wtedy gdy podane
114     # są tylko rodzaj literacki i autor
115     if 'kind' in self and 'author' in self and len(self) == 2:
116         text = u'%s w twórczości %s' % (
117             unicode(self['kind']), flection.get_case(unicode(self['author']), u'dopełniacz'))
118         return capfirst(text)
119
120     # Przypadki ogólniejsze
121     if 'theme' in self:
122         title += u'Motyw %s' % unicode(self['theme'])
123
124     if 'genre' in self:
125         if 'theme' in self:
126             title += u' w %s' % flection.get_case(unicode(self['genre']), u'miejscownik')
127         else:
128             title += unicode(self['genre'])
129
130     if 'kind' in self or 'author' in self or 'epoch' in self:
131         if 'genre' in self or 'theme' in self:
132             if 'kind' in self:
133                 title += u' w %s ' % flection.get_case(unicode(self['kind']), u'miejscownik')
134             else:
135                 title += u' w twórczości '
136         else:
137             title += u'%s ' % unicode(self.get('kind', u'twórczość'))
138
139     if 'author' in self:
140         title += flection.get_case(unicode(self['author']), u'dopełniacz')
141     elif 'epoch' in self:
142         title += flection.get_case(unicode(self['epoch']), u'dopełniacz')
143
144     return capfirst(title)
145
146
147 @register.simple_tag
148 def book_tree(book_list, books_by_parent):
149     text = "".join("<li><a href='%s'>%s</a>%s</li>" % (
150         book.get_absolute_url(), book.title, book_tree(books_by_parent.get(book, ()), books_by_parent)
151         ) for book in book_list)
152
153     if text:
154         return "<ol>%s</ol>" % text
155     else:
156         return ''
157
158
159 @register.simple_tag
160 def audiobook_tree(book_list, books_by_parent):
161     text = "".join("<li><a class='open-player' href='%s'>%s</a>%s</li>" % (
162         reverse("book_player", args=[book.slug]), book.title,
163         audiobook_tree(books_by_parent.get(book, ()), books_by_parent)
164     ) for book in book_list)
165
166     if text:
167         return "<ol>%s</ol>" % text
168     else:
169         return ''
170
171
172 @register.simple_tag
173 def book_tree_texml(book_list, books_by_parent, depth=1):
174     return "".join("""
175             <cmd name='hspace'><parm>%(depth)dem</parm></cmd>%(title)s
176             <spec cat='align' /><cmd name="note"><parm>%(audiences)s</parm></cmd>
177             <spec cat='align' /><cmd name="note"><parm>%(audiobook)s</parm></cmd>
178             <ctrl ch='\\' />
179             %(children)s
180             """ % {
181                 "depth": depth,
182                 "title": book.title,
183                 "audiences": ", ".join(book.audiences_pl()),
184                 "audiobook": "audiobook" if book.has_media('mp3') else "",
185                 "children": book_tree_texml(books_by_parent.get(book.id, ()), books_by_parent, depth + 1)
186             } for book in book_list)
187
188
189 @register.simple_tag
190 def book_tree_csv(author, book_list, books_by_parent, depth=1, max_depth=3, delimeter="\t"):
191     def quote_if_necessary(s):
192         try:
193             s.index(delimeter)
194             s.replace('"', '\\"')
195             return '"%s"' % s
196         except ValueError:
197             return s
198
199     return "".join("""%(author)s%(d)s%(preindent)s%(title)s%(d)s%(postindent)s%(audiences)s%(d)s%(audiobook)s
200 %(children)s""" % {
201                 "d": delimeter,
202                 "preindent": delimeter * (depth - 1),
203                 "postindent": delimeter * (max_depth - depth),
204                 "depth": depth,
205                 "author": quote_if_necessary(author.name),
206                 "title": quote_if_necessary(book.title),
207                 "audiences": ", ".join(book.audiences_pl()),
208                 "audiobook": "audiobook" if book.has_media('mp3') else "",
209                 "children": book_tree_csv(author, books_by_parent.get(book.id, ()), books_by_parent, depth + 1)
210             } for book in book_list)
211
212
213 @register.simple_tag
214 def all_editors(extra_info):
215     editors = []
216     if 'editors' in extra_info:
217         editors += extra_info['editors']
218     if 'technical_editors' in extra_info:
219         editors += extra_info['technical_editors']
220     # support for extra_info-s from librarian<1.2
221     if 'editor' in extra_info:
222         editors.append(extra_info['editor'])
223     if 'technical_editor' in extra_info:
224         editors.append(extra_info['technical_editor'])
225     return ', '.join(
226                      ' '.join(p.strip() for p in person.rsplit(',', 1)[::-1])
227                      for person in sorted(set(editors)))
228
229
230 @register.simple_tag
231 def user_creation_form():
232     return RegistrationForm(prefix='registration').as_ul()
233
234
235 @register.simple_tag
236 def authentication_form():
237     return LoginForm(prefix='login').as_ul()
238
239
240 @register.tag
241 def catalogue_url(parser, token):
242     bits = token.split_contents()
243
244     tags_to_add = []
245     tags_to_remove = []
246     for bit in bits[2:]:
247         if bit[0] == '-':
248             tags_to_remove.append(bit[1:])
249         else:
250             tags_to_add.append(bit)
251
252     return CatalogueURLNode(bits[1], tags_to_add, tags_to_remove)
253
254
255 class CatalogueURLNode(Node):
256     def __init__(self, list_type, tags_to_add, tags_to_remove):
257         self.tags_to_add = [Variable(tag) for tag in tags_to_add]
258         self.tags_to_remove = [Variable(tag) for tag in tags_to_remove]
259         self.list_type_var = Variable(list_type)
260
261     def render(self, context):
262         list_type = self.list_type_var.resolve(context)
263         tags_to_add = []
264         tags_to_remove = []
265
266         for tag_variable in self.tags_to_add:
267             tag = tag_variable.resolve(context)
268             if isinstance(tag, (list, dict)):
269                 tags_to_add += [t for t in tag]
270             else:
271                 tags_to_add.append(tag)
272
273         for tag_variable in self.tags_to_remove:
274             tag = tag_variable.resolve(context)
275             if iterable(tag):
276                 tags_to_remove += [t for t in tag]
277             else:
278                 tags_to_remove.append(tag)
279
280         tag_slugs = [tag.url_chunk for tag in tags_to_add]
281         for tag in tags_to_remove:
282             try:
283                 tag_slugs.remove(tag.url_chunk)
284             except KeyError:
285                 pass
286
287         if len(tag_slugs) > 0:
288             if list_type == 'gallery':
289                 return reverse('tagged_object_list_gallery', kwargs={'tags': '/'.join(tag_slugs)})
290             elif list_type == 'audiobooks':
291                 return reverse('tagged_object_list_audiobooks', kwargs={'tags': '/'.join(tag_slugs)})
292             else:
293                 return reverse('tagged_object_list', kwargs={'tags': '/'.join(tag_slugs)})
294         else:
295             if list_type == 'gallery':
296                 return reverse('gallery')
297             elif list_type == 'audiobooks':
298                 return reverse('audiobook_list')
299             else:
300                 return reverse('book_list')
301
302
303 # @register.inclusion_tag('catalogue/tag_list.html')
304 def tag_list(tags, choices=None, category=None, list_type='default'):
305     # print(tags, choices, category)
306     if choices is None:
307         choices = []
308
309     if category is None and tags:
310         category = tags[0].category
311
312     category_choices = [tag for tag in choices if tag.category == category]
313
314     if len(tags) == 1 and category not in [t.category for t in choices]:
315         one_tag = tags[0]
316     else:
317         one_tag = None
318
319     if category is not None:
320         other = Tag.objects.filter(category=category).exclude(pk__in=[t.pk for t in tags])\
321             .exclude(pk__in=[t.pk for t in category_choices])
322         # Filter out empty tags.
323         ct = ContentType.objects.get_for_model(Picture if list_type == 'gallery' else Book)
324         other = other.filter(items__content_type=ct).distinct()
325         if list_type == 'audiobooks':
326             audiobook_tag_ids = cache.get('audiobook_tags')
327             if audiobook_tag_ids is None:
328                 books_with_audiobook = Book.objects.filter(media__type__in=('mp3', 'ogg'))\
329                     .distinct().values_list('pk', flat=True)
330                 audiobook_tag_ids = Tag.objects.filter(
331                     items__content_type=ct,
332                     items__object_id__in=list(books_with_audiobook)).distinct().values_list('pk', flat=True)
333                 audiobook_tag_ids = list(audiobook_tag_ids)
334                 cache.set('audiobook_tags', audiobook_tag_ids)
335
336             other = other.filter(id__in=audiobook_tag_ids)
337     else:
338         other = []
339
340     return {
341         'one_tag': one_tag,
342         'choices': choices,
343         'category_choices': category_choices,
344         'tags': tags,
345         'other': other,
346         'list_type': list_type,
347     }
348
349
350 @register.inclusion_tag('catalogue/inline_tag_list.html')
351 def inline_tag_list(tags, choices=None, category=None, list_type='default'):
352     return tag_list(tags, choices, category, list_type)
353
354
355 @register.inclusion_tag('catalogue/collection_list.html')
356 def collection_list(collections):
357     return {'collections': collections}
358
359
360 @register.inclusion_tag('catalogue/book_info.html')
361 def book_info(book):
362     return {
363         'is_picture': isinstance(book, Picture),
364         'book': book,
365     }
366
367
368 @register.inclusion_tag('catalogue/work-list.html', takes_context=True)
369 def work_list(context, object_list):
370     request = context.get('request')
371     return {'object_list': object_list, 'request': request}
372
373
374 @register.inclusion_tag('catalogue/plain_list.html', takes_context=True)
375 def plain_list(context, object_list, with_initials=True, by_author=False, choice=None, book=None, list_type='default',
376                paged=True, initial_blocks=False):
377     names = [('', [])]
378     last_initial = None
379     if len(object_list) < settings.CATALOGUE_MIN_INITIALS and not by_author:
380         with_initials = False
381         initial_blocks = False
382     for obj in object_list:
383         if with_initials:
384             if by_author:
385                 initial = obj.sort_key_author
386             else:
387                 initial = obj.get_initial().upper()
388             if initial != last_initial:
389                 last_initial = initial
390                 names.append((obj.author_str() if by_author else initial, []))
391         names[-1][1].append(obj)
392     return {
393         'paged': paged,
394         'names': names,
395         'initial_blocks': initial_blocks,
396         'book': book,
397         'list_type': list_type,
398         'choice': choice,
399     }
400
401
402 # TODO: These are no longer just books.
403 @register.inclusion_tag('catalogue/related_books.html', takes_context=True)
404 def related_books(context, instance, limit=6, random=1, taken=0):
405     limit -= taken
406     max_books = limit - random
407     is_picture = isinstance(instance, Picture)
408
409     pics_qs = Picture.objects.all()
410     if is_picture:
411         pics_qs = pics_qs.exclude(pk=instance.pk)
412     pics = Picture.tagged.related_to(instance, pics_qs)
413     if pics.exists():
414         # Reserve one spot for an image.
415         max_books -= 1
416
417     books_qs = Book.objects.all()
418     if not is_picture:
419         books_qs = books_qs.exclude(common_slug=instance.common_slug).exclude(ancestor=instance)
420     books = Book.tagged.related_to(instance, books_qs)[:max_books]
421
422     pics = pics[:1 + max_books - books.count()]
423
424     random_excluded_books = [b.pk for b in books]
425     random_excluded_pics = [p.pk for p in pics]
426     (random_excluded_pics if is_picture else random_excluded_books).append(instance.pk)
427
428     return {
429         'request': context['request'],
430         'books': books,
431         'pics': pics,
432         'random': random,
433         'random_excluded_books': random_excluded_books,
434         'random_excluded_pics': random_excluded_pics,
435     }
436
437
438 @register.simple_tag
439 def download_audio(book, daisy=True):
440     links = []
441     if book.has_media('mp3'):
442         links.append("<a href='%s'>%s</a>" % (
443             reverse('download_zip_mp3', args=[book.slug]), BookMedia.formats['mp3'].name))
444     if book.has_media('ogg'):
445         links.append("<a href='%s'>%s</a>" % (
446             reverse('download_zip_ogg', args=[book.slug]), BookMedia.formats['ogg'].name))
447     if daisy and book.has_media('daisy'):
448         for dsy in book.get_media('daisy'):
449             links.append("<a href='%s'>%s</a>" % (dsy.file.url, BookMedia.formats['daisy'].name))
450     return "".join(links)
451
452
453 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
454 def custom_pdf_link_li(book):
455     return {
456         'book': book,
457         'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
458     }
459
460
461 @register.inclusion_tag("catalogue/snippets/license_icon.html")
462 def license_icon(license_url):
463     """Creates a license icon, if the license_url is known."""
464     known = LICENSES.get(license_url)
465     if known is None:
466         return {}
467     return {
468         "license_url": license_url,
469         "icon": "img/licenses/%s.png" % known['icon'],
470         "license_description": known['description'],
471     }
472
473
474 @register.filter
475 def class_name(obj):
476     return obj.__class__.__name__
477
478
479 @register.simple_tag
480 def source_name(url):
481     url = url.lstrip()
482     netloc = urlparse(url).netloc
483     if not netloc:
484         netloc = urlparse('http://' + url).netloc
485     if not netloc:
486         return ''
487     source, created = Source.objects.get_or_create(netloc=netloc)
488     return source.name or netloc
489
490
491 @ssi_variable(register, patch_response=[add_never_cache_headers])
492 def catalogue_random_book(request, exclude_ids):
493     from .. import app_settings
494     if random() < app_settings.RELATED_RANDOM_PICTURE_CHANCE:
495         return None
496     queryset = Book.objects.exclude(pk__in=exclude_ids)
497     count = queryset.count()
498     if count:
499         return queryset[randint(0, count - 1)].pk
500     else:
501         return None
502
503
504 @ssi_variable(register, patch_response=[add_never_cache_headers])
505 def choose_fragment(request, book_id=None, tag_ids=None, unless=False):
506     if unless:
507         return None
508
509     if book_id is not None:
510         fragment = Book.objects.get(pk=book_id).choose_fragment()
511     else:
512         if tag_ids is not None:
513             tags = Tag.objects.filter(pk__in=tag_ids)
514             fragments = Fragment.tagged.with_all(tags).order_by().only('id')
515         else:
516             fragments = Fragment.objects.all().order_by().only('id')
517         fragment_count = fragments.count()
518         fragment = fragments[randint(0, fragment_count - 1)] if fragment_count else None
519     return fragment.pk if fragment is not None else None
520
521
522 @register.filter
523 def strip_tag(html, tag_name):
524     # docelowo może być warto zainstalować BeautifulSoup do takich rzeczy
525     import re
526     return re.sub(r"<.?%s\b[^>]*>" % tag_name, "", html)