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 datetime import datetime
7 from django.db import models
8 from django.db.models import permalink, Q
10 from django.core.cache import cache
11 from django.utils.translation import ugettext_lazy as _
12 from django.contrib.auth.models import User
13 from django.template.loader import render_to_string
14 from django.utils.datastructures import SortedDict
15 from django.utils.safestring import mark_safe
16 from django.utils.translation import get_language
17 from django.core.urlresolvers import reverse
18 from django.db.models.signals import post_save, m2m_changed, pre_delete
20 from django.conf import settings
22 from newtagging.models import TagBase, tags_updated
23 from newtagging import managers
24 from catalogue.fields import JSONField, OverwritingFileField
25 from catalogue.utils import create_zip
26 from shutil import copy
32 ('author', _('author')),
33 ('epoch', _('epoch')),
35 ('genre', _('genre')),
36 ('theme', _('theme')),
42 ('odt', _('ODT file')),
43 ('mp3', _('MP3 file')),
44 ('ogg', _('OGG file')),
45 ('daisy', _('DAISY file')),
48 # not quite, but Django wants you to set a timeout
49 CACHE_FOREVER = 2419200 # 28 days
52 class TagSubcategoryManager(models.Manager):
53 def __init__(self, subcategory):
54 super(TagSubcategoryManager, self).__init__()
55 self.subcategory = subcategory
57 def get_query_set(self):
58 return super(TagSubcategoryManager, self).get_query_set().filter(category=self.subcategory)
62 name = models.CharField(_('name'), max_length=50, db_index=True)
63 slug = models.SlugField(_('slug'), max_length=120, db_index=True)
64 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True)
65 category = models.CharField(_('category'), max_length=50, blank=False, null=False,
66 db_index=True, choices=TAG_CATEGORIES)
67 description = models.TextField(_('description'), blank=True)
68 main_page = models.BooleanField(_('main page'), default=False, db_index=True, help_text=_('Show tag on main page'))
70 user = models.ForeignKey(User, blank=True, null=True)
71 book_count = models.IntegerField(_('book count'), blank=True, null=True)
72 gazeta_link = models.CharField(blank=True, max_length=240)
73 wiki_link = models.CharField(blank=True, max_length=240)
75 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
76 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
78 class UrlDeprecationWarning(DeprecationWarning):
89 categories_dict = dict((item[::-1] for item in categories_rev.iteritems()))
92 ordering = ('sort_key',)
93 verbose_name = _('tag')
94 verbose_name_plural = _('tags')
95 unique_together = (("slug", "category"),)
97 def __unicode__(self):
101 return "Tag(slug=%r)" % self.slug
104 def get_absolute_url(self):
105 return ('catalogue.views.tagged_object_list', [self.url_chunk])
107 def has_description(self):
108 return len(self.description) > 0
109 has_description.short_description = _('description')
110 has_description.boolean = True
113 """ returns global book count for book tags, fragment count for themes """
115 if self.book_count is None:
116 if self.category == 'book':
118 objects = Book.objects.none()
119 elif self.category == 'theme':
120 objects = Fragment.tagged.with_all((self,))
122 objects = Book.tagged.with_all((self,)).order_by()
123 if self.category != 'set':
124 # eliminate descendants
125 l_tags = Tag.objects.filter(slug__in=[book.book_tag_slug() for book in objects])
126 descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)]
128 objects = objects.exclude(pk__in=descendants_keys)
129 self.book_count = objects.count()
131 return self.book_count
134 def get_tag_list(tags):
135 if isinstance(tags, basestring):
140 tags_splitted = tags.split('/')
141 for name in tags_splitted:
143 real_tags.append(Tag.objects.get(slug=name, category=category))
145 elif name in Tag.categories_rev:
146 category = Tag.categories_rev[name]
149 real_tags.append(Tag.objects.exclude(category='book').get(slug=name))
151 except Tag.MultipleObjectsReturned, e:
152 ambiguous_slugs.append(name)
155 # something strange left off
156 raise Tag.DoesNotExist()
158 # some tags should be qualified
159 e = Tag.MultipleObjectsReturned()
161 e.ambiguous_slugs = ambiguous_slugs
164 e = Tag.UrlDeprecationWarning()
169 return TagBase.get_tag_list(tags)
173 return '/'.join((Tag.categories_dict[self.category], self.slug))
176 def get_dynamic_path(media, filename, ext=None, maxlen=100):
177 from slughifi import slughifi
179 # how to put related book's slug here?
181 if media.type == 'daisy':
185 if media is None or not media.name:
186 name = slughifi(filename.split(".")[0])
188 name = slughifi(media.name)
189 return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext)
192 # TODO: why is this hard-coded ?
193 def book_upload_path(ext=None, maxlen=100):
194 return lambda *args: get_dynamic_path(*args, ext=ext, maxlen=maxlen)
197 class BookMedia(models.Model):
198 type = models.CharField(_('type'), choices=MEDIA_FORMATS, max_length="100")
199 name = models.CharField(_('name'), max_length="100")
200 file = OverwritingFileField(_('file'), upload_to=book_upload_path())
201 uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False)
202 extra_info = JSONField(_('extra information'), default='{}', editable=False)
203 book = models.ForeignKey('Book', related_name='media')
204 source_sha1 = models.CharField(null=True, blank=True, max_length=40, editable=False)
206 def __unicode__(self):
207 return "%s (%s)" % (self.name, self.file.name.split("/")[-1])
210 ordering = ('type', 'name')
211 verbose_name = _('book media')
212 verbose_name_plural = _('book media')
214 def save(self, *args, **kwargs):
215 from slughifi import slughifi
216 from catalogue.utils import ExistingFile, remove_zip
219 old = BookMedia.objects.get(pk=self.pk)
220 except BookMedia.DoesNotExist, e:
223 # if name changed, change the file name, too
224 if slughifi(self.name) != slughifi(old.name):
225 self.file.save(None, ExistingFile(self.file.path), save=False, leave=True)
227 super(BookMedia, self).save(*args, **kwargs)
229 # remove the zip package for book with modified media
230 remove_zip(self.book.slug)
232 extra_info = self.get_extra_info_value()
233 extra_info.update(self.read_meta())
234 self.set_extra_info_value(extra_info)
235 self.source_sha1 = self.read_source_sha1(self.file.path, self.type)
236 return super(BookMedia, self).save(*args, **kwargs)
240 Reads some metadata from the audiobook.
243 from mutagen import id3
245 artist_name = director_name = project = funded_by = ''
246 if self.type == 'mp3':
248 audio = id3.ID3(self.file.path)
249 artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
250 director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
251 project = ", ".join([t.data for t in audio.getall('PRIV')
252 if t.owner=='wolnelektury.pl?project'])
253 funded_by = ", ".join([t.data for t in audio.getall('PRIV')
254 if t.owner=='wolnelektury.pl?funded_by'])
257 elif self.type == 'ogg':
259 audio = mutagen.File(self.file.path)
260 artist_name = ', '.join(audio.get('artist', []))
261 director_name = ', '.join(audio.get('conductor', []))
262 project = ", ".join(audio.get('project', []))
263 funded_by = ", ".join(audio.get('funded_by', []))
268 return {'artist_name': artist_name, 'director_name': director_name,
269 'project': project, 'funded_by': funded_by}
272 def read_source_sha1(filepath, filetype):
274 Reads source file SHA1 from audiobok metadata.
277 from mutagen import id3
279 if filetype == 'mp3':
281 audio = id3.ID3(filepath)
282 return [t.data for t in audio.getall('PRIV')
283 if t.owner=='wolnelektury.pl?flac_sha1'][0]
286 elif filetype == 'ogg':
288 audio = mutagen.File(filepath)
289 return audio.get('flac_sha1', [None])[0]
296 class Book(models.Model):
297 title = models.CharField(_('title'), max_length=120)
298 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
299 slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
300 description = models.TextField(_('description'), blank=True)
301 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
302 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
303 parent_number = models.IntegerField(_('parent number'), default=0)
304 extra_info = JSONField(_('extra information'), default='{}')
305 gazeta_link = models.CharField(blank=True, max_length=240)
306 wiki_link = models.CharField(blank=True, max_length=240)
307 # files generated during publication
309 file_types = ['epub', 'html', 'mobi', 'pdf', 'txt', 'xml']
311 parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
312 objects = models.Manager()
313 tagged = managers.ModelTaggedItemManager(Tag)
314 tags = managers.TagDescriptor(Tag)
316 html_built = django.dispatch.Signal()
317 published = django.dispatch.Signal()
319 class AlreadyExists(Exception):
323 ordering = ('sort_key',)
324 verbose_name = _('book')
325 verbose_name_plural = _('books')
327 def __unicode__(self):
330 def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
331 from sortify import sortify
333 self.sort_key = sortify(self.title)
335 ret = super(Book, self).save(force_insert, force_update)
338 self.reset_short_html()
343 def get_absolute_url(self):
344 return ('catalogue.views.book_detail', [self.slug])
350 def book_tag_slug(self):
351 return ('l-' + self.slug)[:120]
354 slug = self.book_tag_slug()
355 book_tag, created = Tag.objects.get_or_create(slug=slug, category='book')
357 book_tag.name = self.title[:50]
358 book_tag.sort_key = self.title.lower()
362 def has_media(self, type):
363 if type in Book.file_types:
364 return bool(getattr(self, "%s_file" % type))
366 return self.media.filter(type=type).exists()
368 def get_media(self, type):
369 if self.has_media(type):
370 if type in Book.file_types:
371 return getattr(self, "%s_file" % type)
373 return self.media.filter(type=type)
378 return self.get_media("mp3")
380 return self.get_media("odt")
382 return self.get_media("ogg")
384 return self.get_media("daisy")
386 def reset_short_html(self):
390 cache_key = "Book.short_html/%d/%s"
391 for lang, langname in settings.LANGUAGES:
392 cache.delete(cache_key % (self.id, lang))
393 # Fragment.short_html relies on book's tags, so reset it here too
394 for fragm in self.fragments.all():
395 fragm.reset_short_html()
397 def short_html(self):
399 cache_key = "Book.short_html/%d/%s" % (self.id, get_language())
400 short_html = cache.get(cache_key)
404 if short_html is not None:
405 return mark_safe(short_html)
407 tags = self.tags.filter(~Q(category__in=('set', 'theme', 'book')))
408 tags = [mark_safe(u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name)) for tag in tags]
411 # files generated during publication
412 if self.has_media("html"):
413 formats.append(u'<a href="%s">%s</a>' % (reverse('book_text', kwargs={'slug': self.slug}), _('Read online')))
414 if self.has_media("pdf"):
415 formats.append(u'<a href="%s">PDF</a>' % self.get_media('pdf').url)
416 if self.has_media("mobi"):
417 formats.append(u'<a href="%s">MOBI</a>' % self.get_media('mobi').url)
418 if self.root_ancestor.has_media("epub"):
419 formats.append(u'<a href="%s">EPUB</a>' % self.root_ancestor.get_media('epub').url)
420 if self.has_media("txt"):
421 formats.append(u'<a href="%s">TXT</a>' % self.get_media('txt').url)
423 for m in self.media.order_by('type'):
424 formats.append(u'<a href="%s">%s</a>' % (m.file.url, m.type.upper()))
426 formats = [mark_safe(format) for format in formats]
428 short_html = unicode(render_to_string('catalogue/book_short.html',
429 {'book': self, 'tags': tags, 'formats': formats}))
432 cache.set(cache_key, short_html, CACHE_FOREVER)
433 return mark_safe(short_html)
436 def root_ancestor(self):
437 """ returns the oldest ancestor """
439 if not hasattr(self, '_root_ancestor'):
443 self._root_ancestor = book
444 return self._root_ancestor
447 def has_description(self):
448 return len(self.description) > 0
449 has_description.short_description = _('description')
450 has_description.boolean = True
453 def has_odt_file(self):
454 return bool(self.has_media("odt"))
455 has_odt_file.short_description = 'ODT'
456 has_odt_file.boolean = True
458 def has_mp3_file(self):
459 return bool(self.has_media("mp3"))
460 has_mp3_file.short_description = 'MP3'
461 has_mp3_file.boolean = True
463 def has_ogg_file(self):
464 return bool(self.has_media("ogg"))
465 has_ogg_file.short_description = 'OGG'
466 has_ogg_file.boolean = True
468 def has_daisy_file(self):
469 return bool(self.has_media("daisy"))
470 has_daisy_file.short_description = 'DAISY'
471 has_daisy_file.boolean = True
473 def build_pdf(self, customizations=None, file_name=None):
474 """ (Re)builds the pdf file.
475 customizations - customizations which are passed to LaTeX class file.
476 file_name - save the pdf file under a different name and DO NOT save it in db.
478 from tempfile import NamedTemporaryFile
479 from os import unlink
480 from django.core.files import File
481 from librarian import pdf
482 from catalogue.utils import ORMDocProvider, remove_zip
483 from django.core.files.move import file_move_safe
486 pdf_file = NamedTemporaryFile(delete=False)
487 pdf.transform(ORMDocProvider(self),
488 file_path=str(self.xml_file.path),
489 output_file=pdf_file,
490 customizations=customizations
493 if file_name is None:
494 self.pdf_file.save('%s.pdf' % self.slug, File(open(pdf_file.name)))
496 copy(pdf_file.name, path.join(settings.MEDIA_ROOT, get_dynamic_path(None, file_name, ext='pdf')))
498 unlink(pdf_file.name)
500 # remove zip with all pdf files
501 remove_zip(settings.ALL_PDF_ZIP)
503 def build_mobi(self):
504 """ (Re)builds the MOBI file.
507 from tempfile import NamedTemporaryFile
508 from os import unlink
509 from django.core.files import File
510 from librarian import mobi
511 from catalogue.utils import ORMDocProvider, remove_zip
514 mobi_file = NamedTemporaryFile(suffix='.mobi', delete=False)
515 mobi.transform(ORMDocProvider(self), verbose=1,
516 file_path=str(self.xml_file.path),
517 output_file=mobi_file.name,
520 self.mobi_file.save('%s.mobi' % self.slug, File(open(mobi_file.name)))
522 unlink(mobi_file.name)
524 # remove zip with all mobi files
525 remove_zip(settings.ALL_MOBI_ZIP)
527 def build_epub(self, remove_descendants=True):
528 """ (Re)builds the epub file.
529 If book has a parent, does nothing.
530 Unless remove_descendants is False, descendants' epubs are removed.
532 from StringIO import StringIO
533 from hashlib import sha1
534 from django.core.files.base import ContentFile
535 from librarian import epub, NoDublinCore
536 from catalogue.utils import ORMDocProvider, remove_zip
542 epub_file = StringIO()
544 epub.transform(ORMDocProvider(self), self.slug, output_file=epub_file)
545 self.epub_file.save('%s.epub' % self.slug, ContentFile(epub_file.getvalue()))
546 FileRecord(slug=self.slug, type='epub', sha1=sha1(epub_file.getvalue()).hexdigest()).save()
550 book_descendants = list(self.children.all())
551 while len(book_descendants) > 0:
552 child_book = book_descendants.pop(0)
553 if remove_descendants and child_book.has_epub_file():
554 child_book.epub_file.delete()
555 # save anyway, to refresh short_html
557 book_descendants += list(child_book.children.all())
559 # remove zip package with all epub files
560 remove_zip(settings.ALL_EPUB_ZIP)
563 from StringIO import StringIO
564 from django.core.files.base import ContentFile
565 from librarian import text
568 text.transform(open(self.xml_file.path), out)
569 self.txt_file.save('%s.txt' % self.slug, ContentFile(out.getvalue()))
572 def build_html(self):
573 from tempfile import NamedTemporaryFile
574 from markupstring import MarkupString
575 from django.core.files import File
576 from slughifi import slughifi
577 from librarian import html
579 meta_tags = list(self.tags.filter(
580 category__in=('author', 'epoch', 'genre', 'kind')))
581 book_tag = self.book_tag()
583 html_file = NamedTemporaryFile()
584 if html.transform(self.xml_file.path, html_file, parse_dublincore=False):
585 self.html_file.save('%s.html' % self.slug, File(html_file))
587 # get ancestor l-tags for adding to new fragments
591 ancestor_tags.append(p.book_tag())
594 # Delete old fragments and create them from scratch
595 self.fragments.all().delete()
597 closed_fragments, open_fragments = html.extract_fragments(self.html_file.path)
598 for fragment in closed_fragments.values():
600 theme_names = [s.strip() for s in fragment.themes.split(',')]
601 except AttributeError:
604 for theme_name in theme_names:
607 tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name), category='theme')
609 tag.name = theme_name
610 tag.sort_key = theme_name.lower()
616 text = fragment.to_string()
618 if (len(MarkupString(text)) > 240):
619 short_text = unicode(MarkupString(text)[:160])
620 new_fragment = Fragment.objects.create(anchor=fragment.id, book=self,
621 text=text, short_text=short_text)
624 new_fragment.tags = set(meta_tags + themes + [book_tag] + ancestor_tags)
626 self.html_built.send(sender=self)
631 def zip_format(format_):
632 def pretty_file_name(book):
633 return "%s/%s.%s" % (
634 b.get_extra_info_value()['author'],
638 field_name = "%s_file" % format_
639 books = Book.objects.filter(parent=None).exclude(**{field_name: ""})
640 paths = [(pretty_file_name(b), getattr(b, field_name).path)
642 result = create_zip.delay(paths,
643 getattr(settings, "ALL_%s_ZIP" % format_.upper()))
646 def zip_audiobooks(self):
647 bm = BookMedia.objects.filter(book=self, type='mp3')
648 paths = map(lambda bm: (None, bm.file.path), bm)
649 result = create_zip.delay(paths, self.slug)
653 def from_xml_file(cls, xml_file, **kwargs):
654 from django.core.files import File
655 from librarian import dcparser
657 # use librarian to parse meta-data
658 book_info = dcparser.parse(xml_file)
660 if not isinstance(xml_file, File):
661 xml_file = File(open(xml_file))
664 return cls.from_text_and_meta(xml_file, book_info, **kwargs)
669 def from_text_and_meta(cls, raw_file, book_info, overwrite=False,
670 build_epub=True, build_txt=True, build_pdf=True, build_mobi=True):
672 from slughifi import slughifi
673 from sortify import sortify
675 # check for parts before we do anything
677 if hasattr(book_info, 'parts'):
678 for part_url in book_info.parts:
679 base, slug = part_url.rsplit('/', 1)
681 children.append(Book.objects.get(slug=slug))
682 except Book.DoesNotExist, e:
683 raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug)
687 book_base, book_slug = book_info.url.rsplit('/', 1)
688 if re.search(r'[^a-zA-Z0-9-]', book_slug):
689 raise ValueError('Invalid characters in slug')
690 book, created = Book.objects.get_or_create(slug=book_slug)
696 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
697 # Save shelves for this book
698 book_shelves = list(book.tags.filter(category='set'))
700 book.title = book_info.title
701 book.set_extra_info_value(book_info.to_dict())
705 categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
706 for field_name, category in categories:
708 tag_names = getattr(book_info, field_name)
710 tag_names = [getattr(book_info, category)]
711 for tag_name in tag_names:
712 tag_sort_key = tag_name
713 if category == 'author':
714 tag_sort_key = tag_name.last_name
715 tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name
716 tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category)
719 tag.sort_key = sortify(tag_sort_key.lower())
721 meta_tags.append(tag)
723 book.tags = set(meta_tags + book_shelves)
725 book_tag = book.book_tag()
727 for n, child_book in enumerate(children):
728 child_book.parent = book
729 child_book.parent_number = n
732 # Save XML and HTML files
733 book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
735 # delete old fragments when overwriting
736 book.fragments.all().delete()
738 if book.build_html():
739 if not settings.NO_BUILD_TXT and build_txt:
742 if not settings.NO_BUILD_EPUB and build_epub:
743 book.root_ancestor.build_epub()
745 if not settings.NO_BUILD_PDF and build_pdf:
746 book.root_ancestor.build_pdf()
748 if not settings.NO_BUILD_MOBI and build_mobi:
751 book_descendants = list(book.children.all())
752 # add l-tag to descendants and their fragments
753 # delete unnecessary EPUB files
754 while len(book_descendants) > 0:
755 child_book = book_descendants.pop(0)
756 child_book.tags = list(child_book.tags) + [book_tag]
758 for fragment in child_book.fragments.all():
759 fragment.tags = set(list(fragment.tags) + [book_tag])
760 book_descendants += list(child_book.children.all())
765 book.reset_tag_counter()
766 book.reset_theme_counter()
768 cls.published.send(sender=book)
771 def reset_tag_counter(self):
775 cache_key = "Book.tag_counter/%d" % self.id
776 cache.delete(cache_key)
778 self.parent.reset_tag_counter()
781 def tag_counter(self):
783 cache_key = "Book.tag_counter/%d" % self.id
784 tags = cache.get(cache_key)
790 for child in self.children.all().order_by():
791 for tag_pk, value in child.tag_counter.iteritems():
792 tags[tag_pk] = tags.get(tag_pk, 0) + value
793 for tag in self.tags.exclude(category__in=('book', 'theme', 'set')).order_by():
797 cache.set(cache_key, tags, CACHE_FOREVER)
800 def reset_theme_counter(self):
804 cache_key = "Book.theme_counter/%d" % self.id
805 cache.delete(cache_key)
807 self.parent.reset_theme_counter()
810 def theme_counter(self):
812 cache_key = "Book.theme_counter/%d" % self.id
813 tags = cache.get(cache_key)
819 for fragment in Fragment.tagged.with_any([self.book_tag()]).order_by():
820 for tag in fragment.tags.filter(category='theme').order_by():
821 tags[tag.pk] = tags.get(tag.pk, 0) + 1
824 cache.set(cache_key, tags, CACHE_FOREVER)
827 def pretty_title(self, html_links=False):
829 names = list(book.tags.filter(category='author'))
835 names.extend(reversed(books))
838 names = ['<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) for tag in names]
840 names = [tag.name for tag in names]
842 return ', '.join(names)
845 def tagged_top_level(cls, tags):
846 """ Returns top-level books tagged with `tags'.
848 It only returns those books which don't have ancestors which are
849 also tagged with those tags.
852 # get relevant books and their tags
853 objects = cls.tagged.with_all(tags)
854 # eliminate descendants
855 l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in objects])
856 descendants_keys = [book.pk for book in cls.tagged.with_any(l_tags)]
858 objects = objects.exclude(pk__in=descendants_keys)
863 def book_list(cls, filter=None):
864 """Generates a hierarchical listing of all books.
866 Books are optionally filtered with a test function.
871 books = cls.objects.all().order_by('parent_number', 'sort_key').only('title', 'parent', 'slug')
873 books = books.filter(filter).distinct()
874 book_ids = set((book.pk for book in books))
876 parent = book.parent_id
877 if parent not in book_ids:
879 books_by_parent.setdefault(parent, []).append(book)
882 books_by_parent.setdefault(book.parent_id, []).append(book)
885 books_by_author = SortedDict()
886 for tag in Tag.objects.filter(category='author'):
887 books_by_author[tag] = []
889 for book in books_by_parent.get(None,()):
890 authors = list(book.tags.filter(category='author'))
892 for author in authors:
893 books_by_author[author].append(book)
897 return books_by_author, orphans, books_by_parent
900 "SP1": (1, u"szkoła podstawowa"),
901 "SP2": (1, u"szkoła podstawowa"),
902 "P": (1, u"szkoła podstawowa"),
903 "G": (2, u"gimnazjum"),
905 "LP": (3, u"liceum"),
907 def audiences_pl(self):
908 audiences = self.get_extra_info_value().get('audiences', [])
909 audiences = sorted(set([self._audiences_pl[a] for a in audiences]))
910 return [a[1] for a in audiences]
913 def _has_factory(ftype):
914 has = lambda self: bool(getattr(self, "%s_file" % ftype))
915 has.short_description = t.upper()
917 has.__name__ = "has_%s_file" % ftype
921 # add the file fields
922 for t in Book.file_types:
923 field_name = "%s_file" % t
924 models.FileField(_("%s file" % t.upper()),
925 upload_to=book_upload_path(t),
926 blank=True).contribute_to_class(Book, field_name)
928 setattr(Book, "has_%s_file" % t, _has_factory(t))
931 class Fragment(models.Model):
932 text = models.TextField()
933 short_text = models.TextField(editable=False)
934 anchor = models.CharField(max_length=120)
935 book = models.ForeignKey(Book, related_name='fragments')
937 objects = models.Manager()
938 tagged = managers.ModelTaggedItemManager(Tag)
939 tags = managers.TagDescriptor(Tag)
942 ordering = ('book', 'anchor',)
943 verbose_name = _('fragment')
944 verbose_name_plural = _('fragments')
946 def get_absolute_url(self):
947 return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
949 def reset_short_html(self):
953 cache_key = "Fragment.short_html/%d/%s"
954 for lang, langname in settings.LANGUAGES:
955 cache.delete(cache_key % (self.id, lang))
957 def short_html(self):
959 cache_key = "Fragment.short_html/%d/%s" % (self.id, get_language())
960 short_html = cache.get(cache_key)
964 if short_html is not None:
965 return mark_safe(short_html)
967 short_html = unicode(render_to_string('catalogue/fragment_short.html',
970 cache.set(cache_key, short_html, CACHE_FOREVER)
971 return mark_safe(short_html)
974 class FileRecord(models.Model):
975 slug = models.SlugField(_('slug'), max_length=120, db_index=True)
976 type = models.CharField(_('type'), max_length=20, db_index=True)
977 sha1 = models.CharField(_('sha-1 hash'), max_length=40)
978 time = models.DateTimeField(_('time'), auto_now_add=True)
981 ordering = ('-time','-slug', '-type')
982 verbose_name = _('file record')
983 verbose_name_plural = _('file records')
985 def __unicode__(self):
986 return "%s %s.%s" % (self.sha1, self.slug, self.type)
995 def _tags_updated_handler(sender, affected_tags, **kwargs):
996 # reset tag global counter
997 # we want Tag.changed_at updated for API to know the tag was touched
998 Tag.objects.filter(pk__in=[tag.pk for tag in affected_tags]).update(book_count=None, changed_at=datetime.now())
1000 # if book tags changed, reset book tag counter
1001 if isinstance(sender, Book) and \
1002 Tag.objects.filter(pk__in=(tag.pk for tag in affected_tags)).\
1003 exclude(category__in=('book', 'theme', 'set')).count():
1004 sender.reset_tag_counter()
1005 # if fragment theme changed, reset book theme counter
1006 elif isinstance(sender, Fragment) and \
1007 Tag.objects.filter(pk__in=(tag.pk for tag in affected_tags)).\
1008 filter(category='theme').count():
1009 sender.book.reset_theme_counter()
1010 tags_updated.connect(_tags_updated_handler)
1013 def _pre_delete_handler(sender, instance, **kwargs):
1014 """ refresh Book on BookMedia delete """
1015 if sender == BookMedia:
1016 instance.book.save()
1017 pre_delete.connect(_pre_delete_handler)
1019 def _post_save_handler(sender, instance, **kwargs):
1020 """ refresh all the short_html stuff on BookMedia update """
1021 if sender == BookMedia:
1022 instance.book.save()
1023 post_save.connect(_post_save_handler)