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.
5 from random import randint, random
6 from urlparse import urlparse
7 from django.contrib.contenttypes.models import ContentType
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 _
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
23 register = template.Library()
26 class RegistrationForm(UserCreationForm):
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)
34 class LoginForm(AuthenticationForm):
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)
52 return '%s%s' % (text[0].upper(), text[1:])
58 def html_title_from_tags(tags):
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))
66 def simple_title(tags):
69 title.append("%s: %s" % (_(tag.category), tag.name))
70 return capfirst(', '.join(title))
74 def book_title(book, html_links=False):
75 return book.pretty_title(html_links)
79 def book_title_html(book):
80 return book_title(book, html_links=True)
84 def title_from_tags(tags):
88 result[tag.category] = tag
91 # TODO: Remove this after adding flection mechanism
92 return simple_title(tags)
94 class Flection(object):
95 def get_case(self, name, flection):
99 self = split_tags(tags)
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']
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)
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)
120 # Przypadki ogólniejsze
122 title += u'Motyw %s' % unicode(self['theme'])
126 title += u' w %s' % flection.get_case(unicode(self['genre']), u'miejscownik')
128 title += unicode(self['genre'])
130 if 'kind' in self or 'author' in self or 'epoch' in self:
131 if 'genre' in self or 'theme' in self:
133 title += u' w %s ' % flection.get_case(unicode(self['kind']), u'miejscownik')
135 title += u' w twórczości '
137 title += u'%s ' % unicode(self.get('kind', u'twórczość'))
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')
144 return capfirst(title)
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)
154 return "<ol>%s</ol>" % text
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)
167 return "<ol>%s</ol>" % text
173 def book_tree_texml(book_list, books_by_parent, depth=1):
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>
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)
190 def book_tree_csv(author, book_list, books_by_parent, depth=1, max_depth=3, delimeter="\t"):
191 def quote_if_necessary(s):
194 s.replace('"', '\\"')
199 return "".join("""%(author)s%(d)s%(preindent)s%(title)s%(d)s%(postindent)s%(audiences)s%(d)s%(audiobook)s
202 "preindent": delimeter * (depth - 1),
203 "postindent": delimeter * (max_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)
214 def all_editors(extra_info):
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'])
226 ' '.join(p.strip() for p in person.rsplit(',', 1)[::-1])
227 for person in sorted(set(editors)))
231 def user_creation_form():
232 return RegistrationForm(prefix='registration').as_ul()
236 def authentication_form():
237 return LoginForm(prefix='login').as_ul()
241 def catalogue_url(parser, token):
242 bits = token.split_contents()
248 tags_to_remove.append(bit[1:])
250 tags_to_add.append(bit)
252 return CatalogueURLNode(bits[1], tags_to_add, tags_to_remove)
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)
261 def render(self, context):
262 list_type = self.list_type_var.resolve(context)
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]
271 tags_to_add.append(tag)
273 for tag_variable in self.tags_to_remove:
274 tag = tag_variable.resolve(context)
276 tags_to_remove += [t for t in tag]
278 tags_to_remove.append(tag)
280 tag_slugs = [tag.url_chunk for tag in tags_to_add]
281 for tag in tags_to_remove:
283 tag_slugs.remove(tag.url_chunk)
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)})
293 return reverse('tagged_object_list', kwargs={'tags': '/'.join(tag_slugs)})
295 if list_type == 'gallery':
296 return reverse('gallery')
297 elif list_type == 'audiobooks':
298 return reverse('audiobook_list')
300 return reverse('book_list')
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)
309 if category is None and tags:
310 category = tags[0].category
312 category_choices = [tag for tag in choices if tag.category == category]
314 if len(tags) == 1 and category not in [t.category for t in choices]:
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)
336 other = other.filter(id__in=audiobook_tag_ids)
343 'category_choices': category_choices,
346 'list_type': list_type,
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)
355 @register.inclusion_tag('catalogue/collection_list.html')
356 def collection_list(collections):
357 return {'collections': collections}
360 @register.inclusion_tag('catalogue/book_info.html')
363 'is_picture': isinstance(book, Picture),
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}
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):
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:
385 initial = obj.sort_key_author
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)
395 'initial_blocks': initial_blocks,
397 'list_type': list_type,
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):
406 max_books = limit - random
407 is_picture = isinstance(instance, Picture)
409 pics_qs = Picture.objects.all()
411 pics_qs = pics_qs.exclude(pk=instance.pk)
412 pics = Picture.tagged.related_to(instance, pics_qs)
414 # Reserve one spot for an image.
417 books_qs = Book.objects.all()
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]
422 pics = pics[:1 + max_books - books.count()]
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)
429 'request': context['request'],
433 'random_excluded_books': random_excluded_books,
434 'random_excluded_pics': random_excluded_pics,
439 def download_audio(book, daisy=True):
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)
453 @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html")
454 def custom_pdf_link_li(book):
457 'NO_CUSTOM_PDF': settings.NO_CUSTOM_PDF,
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)
468 "license_url": license_url,
469 "icon": "img/licenses/%s.png" % known['icon'],
470 "license_description": known['description'],
476 return obj.__class__.__name__
480 def source_name(url):
482 netloc = urlparse(url).netloc
484 netloc = urlparse('http://' + url).netloc
487 source, created = Source.objects.get_or_create(netloc=netloc)
488 return source.name or netloc
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:
496 queryset = Book.objects.exclude(pk__in=exclude_ids)
497 count = queryset.count()
499 return queryset[randint(0, count - 1)].pk
504 @ssi_variable(register, patch_response=[add_never_cache_headers])
505 def choose_fragment(request, book_id=None, tag_ids=None, unless=False):
509 if book_id is not None:
510 fragment = Book.objects.get(pk=book_id).choose_fragment()
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')
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
523 def strip_tag(html, tag_name):
524 # docelowo może być warto zainstalować BeautifulSoup do takich rzeczy
526 return re.sub(r"<.?%s\b[^>]*>" % tag_name, "", html)