1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 from collections import OrderedDict
6 from datetime import date, timedelta
7 from random import randint
10 from urllib.request import urlretrieve
11 from django.conf import settings
12 from django.db import connection, models, transaction
13 import django.dispatch
14 from django.contrib.contenttypes.fields import GenericRelation
15 from django.template.loader import render_to_string
16 from django.urls import reverse
17 from django.utils.translation import ugettext_lazy as _, get_language
18 from django.utils.deconstruct import deconstructible
19 from fnpdjango.storage import BofhFileSystemStorage
21 from librarian.cover import WLCover
22 from librarian.html import transform_abstrakt
23 from newtagging import managers
24 from catalogue import constants
25 from catalogue.fields import EbookField
26 from catalogue.models import Tag, Fragment, BookMedia
27 from catalogue.utils import create_zip, gallery_url, gallery_path, split_tags, get_random_hash
28 from catalogue.models.tag import prefetched_relations
29 from catalogue import app_settings
30 from catalogue import tasks
31 from wolnelektury.utils import makedirs, cached_render, clear_cached_renders
33 bofh_storage = BofhFileSystemStorage()
37 class UploadToPath(object):
38 def __init__(self, path):
41 def __call__(self, instance, filename):
42 return self.path % instance.slug
45 _cover_upload_to = UploadToPath('book/cover/%s.jpg')
46 _cover_thumb_upload_to = UploadToPath('book/cover_thumb/%s.jpg')
47 _cover_api_thumb_upload_to = UploadToPath('book/cover_api_thumb/%s.jpg')
48 _simple_cover_upload_to = UploadToPath('book/cover_simple/%s.jpg')
49 _cover_ebookpoint_upload_to = UploadToPath('book/cover_ebookpoint/%s.jpg')
52 def _ebook_upload_to(upload_path):
53 return UploadToPath(upload_path)
56 class Book(models.Model):
57 """Represents a book imported from WL-XML."""
58 title = models.CharField(_('title'), max_length=32767)
59 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
60 sort_key_author = models.CharField(
61 _('sort key by author'), max_length=120, db_index=True, editable=False, default='')
62 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
63 common_slug = models.SlugField(_('slug'), max_length=120, db_index=True)
64 language = models.CharField(_('language code'), max_length=3, db_index=True, default=app_settings.DEFAULT_LANGUAGE)
65 description = models.TextField(_('description'), blank=True)
66 abstract = models.TextField(_('abstract'), blank=True)
67 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
68 changed_at = models.DateTimeField(_('change date'), auto_now=True, db_index=True)
69 parent_number = models.IntegerField(_('parent number'), default=0)
70 extra_info = models.TextField(_('extra information'), default='{}')
71 gazeta_link = models.CharField(blank=True, max_length=240)
72 wiki_link = models.CharField(blank=True, max_length=240)
73 print_on_demand = models.BooleanField(_('print on demand'), default=False)
74 recommended = models.BooleanField(_('recommended'), default=False)
75 audio_length = models.CharField(_('audio length'), blank=True, max_length=8)
76 preview = models.BooleanField(_('preview'), default=False)
77 preview_until = models.DateField(_('preview until'), blank=True, null=True)
78 preview_key = models.CharField(max_length=32, blank=True, null=True)
79 findable = models.BooleanField(_('findable'), default=True, db_index=True)
81 # files generated during publication
84 null=True, blank=True,
85 upload_to=_cover_upload_to,
86 storage=bofh_storage, max_length=255)
87 cover_etag = models.CharField(max_length=255, editable=False, default='', db_index=True)
88 # Cleaner version of cover for thumbs
89 cover_thumb = EbookField(
90 'cover_thumb', _('cover thumbnail'),
91 null=True, blank=True,
92 upload_to=_cover_thumb_upload_to,
94 cover_thumb_etag = models.CharField(max_length=255, editable=False, default='', db_index=True)
95 cover_api_thumb = EbookField(
96 'cover_api_thumb', _('cover thumbnail for mobile app'),
97 null=True, blank=True,
98 upload_to=_cover_api_thumb_upload_to,
100 cover_api_thumb_etag = models.CharField(max_length=255, editable=False, default='', db_index=True)
101 simple_cover = EbookField(
102 'simple_cover', _('cover for mobile app'),
103 null=True, blank=True,
104 upload_to=_simple_cover_upload_to,
106 simple_cover_etag = models.CharField(max_length=255, editable=False, default='', db_index=True)
107 cover_ebookpoint = EbookField(
108 'cover_ebookpoint', _('cover for Ebookpoint'),
109 null=True, blank=True,
110 upload_to=_cover_ebookpoint_upload_to,
112 cover_ebookpoint_etag = models.CharField(max_length=255, editable=False, default='', db_index=True)
113 ebook_formats = constants.EBOOK_FORMATS
114 formats = ebook_formats + ['html', 'xml']
116 parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True, related_name='children')
117 ancestor = models.ManyToManyField('self', blank=True, editable=False, related_name='descendant', symmetrical=False)
119 cached_author = models.CharField(blank=True, max_length=240, db_index=True)
120 has_audience = models.BooleanField(default=False)
122 objects = models.Manager()
123 tagged = managers.ModelTaggedItemManager(Tag)
124 tags = managers.TagDescriptor(Tag)
125 tag_relations = GenericRelation(Tag.intermediary_table_model)
127 html_built = django.dispatch.Signal()
128 published = django.dispatch.Signal()
132 class AlreadyExists(Exception):
136 ordering = ('sort_key_author', 'sort_key')
137 verbose_name = _('book')
138 verbose_name_plural = _('books')
139 app_label = 'catalogue'
144 def get_extra_info_json(self):
145 return json.loads(self.extra_info or '{}')
147 def get_initial(self):
149 return re.search(r'\w', self.title, re.U).group(0)
150 except AttributeError:
154 return self.tags.filter(category='author')
157 return self.tags.filter(category='epoch')
160 return self.tags.filter(category='genre')
163 return self.tags.filter(category='kind')
165 def tag_unicode(self, category):
166 relations = prefetched_relations(self, category)
168 return ', '.join(rel.tag.name for rel in relations)
170 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
172 def tags_by_category(self):
173 return split_tags(self.tags.exclude(category__in=('set', 'theme')))
175 def author_unicode(self):
176 return self.cached_author
178 def kind_unicode(self):
179 return self.tag_unicode('kind')
181 def epoch_unicode(self):
182 return self.tag_unicode('epoch')
184 def genre_unicode(self):
185 return self.tag_unicode('genre')
187 def translators(self):
188 translators = self.get_extra_info_json().get('translators') or []
190 '\xa0'.join(reversed(translator.split(', ', 1))) for translator in translators
193 def translator(self):
194 translators = self.get_extra_info_json().get('translators')
197 if len(translators) > 3:
198 translators = translators[:2]
202 return ', '.join('\xa0'.join(reversed(translator.split(', ', 1))) for translator in translators) + others
204 def cover_source(self):
205 return self.get_extra_info_json().get('cover_source', self.parent.cover_source() if self.parent else '')
209 return self.get_extra_info_json().get('isbn_pdf')
213 return self.get_extra_info_json().get('isbn_epub')
217 return self.get_extra_info_json().get('isbn_mobi')
220 def save(self, force_insert=False, force_update=False, **kwargs):
221 from sortify import sortify
223 self.sort_key = sortify(self.title)[:120]
224 self.title = str(self.title) # ???
227 author = self.authors().first().sort_key
228 except AttributeError:
230 self.sort_key_author = author
232 self.cached_author = self.tag_unicode('author')
233 self.has_audience = 'audience' in self.get_extra_info_json()
235 if self.preview and not self.preview_key:
236 self.preview_key = get_random_hash(self.slug)[:32]
238 ret = super(Book, self).save(force_insert, force_update, **kwargs)
242 def get_absolute_url(self):
243 return reverse('book_detail', args=[self.slug])
245 def gallery_path(self):
246 return gallery_path(self.slug)
248 def gallery_url(self):
249 return gallery_url(self.slug)
251 def get_first_text(self):
254 child = self.children.all().order_by('parent_number').first()
255 if child is not None:
256 return child.get_first_text()
258 def get_last_text(self):
261 child = self.children.all().order_by('parent_number').last()
262 if child is not None:
263 return child.get_last_text()
265 def get_prev_text(self):
268 sibling = self.parent.children.filter(parent_number__lt=self.parent_number).order_by('-parent_number').first()
269 if sibling is not None:
270 return sibling.get_last_text()
272 if self.parent.html_file:
275 return self.parent.get_prev_text()
277 def get_next_text(self):
278 child = self.children.order_by('parent_number').first()
279 if child is not None:
280 return child.get_first_text()
284 sibling = self.parent.children.filter(parent_number__gt=self.parent_number).order_by('parent_number').first()
285 if sibling is not None:
286 return sibling.get_first_text()
287 return self.parent.get_next_text()
289 def get_siblings(self):
292 return self.parent.children.all().order_by('parent_number')
294 def get_children(self):
295 return self.children.all().order_by('parent_number')
301 def language_code(self):
302 return constants.LANGUAGES_3TO2.get(self.language, self.language)
304 def language_name(self):
305 return dict(settings.LANGUAGES).get(self.language_code(), "")
307 def is_foreign(self):
308 return self.language_code() != settings.LANGUAGE_CODE
310 def set_audio_length(self):
311 length = self.get_audio_length()
313 self.audio_length = self.format_audio_length(length)
317 def format_audio_length(seconds):
319 >>> Book.format_audio_length(1)
321 >>> Book.format_audio_length(3661)
325 minutes = seconds // 60
326 seconds = seconds % 60
327 return '%d:%02d' % (minutes, seconds)
329 hours = seconds // 3600
330 minutes = seconds % 3600 // 60
331 seconds = seconds % 60
332 return '%d:%02d:%02d' % (hours, minutes, seconds)
334 def get_audio_length(self):
336 for media in self.get_mp3() or ():
337 total += app_settings.GET_MP3_LENGTH(media.file.path)
340 def has_media(self, type_):
341 if type_ in Book.formats:
342 return bool(getattr(self, "%s_file" % type_))
344 return self.media.filter(type=type_).exists()
347 return self.has_media('mp3')
349 def get_media(self, type_):
350 if self.has_media(type_):
351 if type_ in Book.formats:
352 return getattr(self, "%s_file" % type_)
354 return self.media.filter(type=type_)
359 return self.get_media("mp3")
362 return self.get_media("odt")
365 return self.get_media("ogg")
368 return self.get_media("daisy")
370 def media_url(self, format_):
371 media = self.get_media(format_)
374 return reverse('embargo_link', kwargs={'key': self.preview_key, 'slug': self.slug, 'format_': format_})
381 return self.media_url('html')
384 return self.media_url('pdf')
387 return self.media_url('epub')
390 return self.media_url('mobi')
393 return self.media_url('txt')
396 return self.media_url('fb2')
399 return self.media_url('xml')
401 def has_description(self):
402 return len(self.description) > 0
403 has_description.short_description = _('description')
404 has_description.boolean = True
406 def has_mp3_file(self):
407 return self.has_media("mp3")
408 has_mp3_file.short_description = 'MP3'
409 has_mp3_file.boolean = True
411 def has_ogg_file(self):
412 return self.has_media("ogg")
413 has_ogg_file.short_description = 'OGG'
414 has_ogg_file.boolean = True
416 def has_daisy_file(self):
417 return self.has_media("daisy")
418 has_daisy_file.short_description = 'DAISY'
419 has_daisy_file.boolean = True
422 def media_daisy(self):
423 return self.get_media('daisy')
425 def get_audiobooks(self):
427 for m in self.media.filter(type='ogg').order_by().iterator():
428 ogg_files[m.name] = m
432 for mp3 in self.media.filter(type='mp3').iterator():
433 # ogg files are always from the same project
434 meta = mp3.get_extra_info_json()
435 project = meta.get('project')
438 project = 'CzytamySłuchając'
440 projects.add((project, meta.get('funded_by', '')))
444 ogg = ogg_files.get(mp3.name)
447 audiobooks.append(media)
449 projects = sorted(projects)
450 return audiobooks, projects
452 def wldocument(self, parse_dublincore=True, inherit=True):
453 from catalogue.import_utils import ORMDocProvider
454 from librarian.parser import WLDocument
456 if inherit and self.parent:
457 meta_fallbacks = self.parent.cover_info()
459 meta_fallbacks = None
461 return WLDocument.from_file(
463 provider=ORMDocProvider(self),
464 parse_dublincore=parse_dublincore,
465 meta_fallbacks=meta_fallbacks)
467 def wldocument2(self):
468 from catalogue.import_utils import ORMDocProvider
469 from librarian.document import WLDocument
472 provider=ORMDocProvider(self)
474 doc.meta.update(self.cover_info())
479 def zip_format(format_):
480 def pretty_file_name(book):
481 return "%s/%s.%s" % (
482 book.get_extra_info_json()['author'],
486 field_name = "%s_file" % format_
487 books = Book.objects.filter(parent=None).exclude(**{field_name: ""}).exclude(preview=True).exclude(findable=False)
488 paths = [(pretty_file_name(b), getattr(b, field_name).path) for b in books.iterator()]
489 return create_zip(paths, app_settings.FORMAT_ZIPS[format_])
491 def zip_audiobooks(self, format_):
492 bm = BookMedia.objects.filter(book=self, type=format_)
493 paths = map(lambda bm: (bm.get_nice_filename(), bm.file.path), bm)
496 license = constants.LICENSES.get(
497 m.get_extra_info_json().get('license'), {}
500 licenses.add(license)
501 readme = render_to_string('catalogue/audiobook_zip_readme.txt', {
502 'licenses': licenses,
504 return create_zip(paths, "%s_%s" % (self.slug, format_), {'informacje.txt': readme})
506 def search_index(self, book_info=None, index=None, index_tags=True, commit=True):
507 if not self.findable:
510 from search.index import Index
513 index.index_book(self, book_info)
518 except Exception as e:
519 index.index.rollback()
522 # will make problems in conjunction with paid previews
523 def download_pictures(self, remote_gallery_url):
524 gallery_path = self.gallery_path()
525 # delete previous files, so we don't include old files in ebooks
526 if os.path.isdir(gallery_path):
527 for filename in os.listdir(gallery_path):
528 file_path = os.path.join(gallery_path, filename)
530 ilustr_elements = list(self.wldocument().edoc.findall('//ilustr'))
532 makedirs(gallery_path)
533 for ilustr in ilustr_elements:
534 ilustr_src = ilustr.get('src')
535 ilustr_path = os.path.join(gallery_path, ilustr_src)
536 urlretrieve('%s/%s' % (remote_gallery_url, ilustr_src), ilustr_path)
538 def load_abstract(self):
539 abstract = self.wldocument(parse_dublincore=False).edoc.getroot().find('.//abstrakt')
540 if abstract is not None:
541 self.abstract = transform_abstrakt(abstract)
546 def from_xml_file(cls, xml_file, **kwargs):
547 from django.core.files import File
548 from librarian import dcparser
550 # use librarian to parse meta-data
551 book_info = dcparser.parse(xml_file)
553 if not isinstance(xml_file, File):
554 xml_file = File(open(xml_file))
557 return cls.from_text_and_meta(xml_file, book_info, **kwargs)
562 def from_text_and_meta(cls, raw_file, book_info, overwrite=False, dont_build=None, search_index=True,
563 search_index_tags=True, remote_gallery_url=None, days=0, findable=True):
564 if dont_build is None:
566 dont_build = set.union(set(dont_build), set(app_settings.DONT_BUILD))
568 # check for parts before we do anything
570 if hasattr(book_info, 'parts'):
571 for part_url in book_info.parts:
573 children.append(Book.objects.get(slug=part_url.slug))
574 except Book.DoesNotExist:
575 raise Book.DoesNotExist(_('Book "%s" does not exist.') % part_url.slug)
578 book_slug = book_info.url.slug
579 if re.search(r'[^a-z0-9-]', book_slug):
580 raise ValueError('Invalid characters in slug')
581 book, created = Book.objects.get_or_create(slug=book_slug)
586 book.preview = bool(days)
588 book.preview_until = date.today() + timedelta(days)
591 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
592 # Save shelves for this book
593 book_shelves = list(book.tags.filter(category='set'))
594 old_cover = book.cover_info()
597 book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
599 book.xml_file.set_readable(False)
601 book.findable = findable
602 book.language = book_info.language
603 book.title = book_info.title
604 if book_info.variant_of:
605 book.common_slug = book_info.variant_of.slug
607 book.common_slug = book.slug
608 book.extra_info = json.dumps(book_info.to_dict())
612 meta_tags = Tag.tags_from_info(book_info)
614 for tag in meta_tags:
615 if not tag.for_books:
619 book.tags = set(meta_tags + book_shelves)
620 book.save() # update sort_key_author
622 cover_changed = old_cover != book.cover_info()
623 obsolete_children = set(b for b in book.children.all()
624 if b not in children)
625 notify_cover_changed = []
626 for n, child_book in enumerate(children):
627 new_child = child_book.parent != book
628 child_book.parent = book
629 child_book.parent_number = n
631 if new_child or cover_changed:
632 notify_cover_changed.append(child_book)
633 # Disown unfaithful children and let them cope on their own.
634 for child in obsolete_children:
636 child.parent_number = 0
639 notify_cover_changed.append(child)
641 cls.repopulate_ancestors()
642 tasks.update_counters.delay()
644 if remote_gallery_url:
645 book.download_pictures(remote_gallery_url)
647 # No saves beyond this point.
650 if 'cover' not in dont_build:
651 book.cover.build_delay()
652 book.cover_thumb.build_delay()
653 book.cover_api_thumb.build_delay()
654 book.simple_cover.build_delay()
655 book.cover_ebookpoint.build_delay()
657 # Build HTML and ebooks.
658 book.html_file.build_delay()
660 for format_ in constants.EBOOK_FORMATS_WITHOUT_CHILDREN:
661 if format_ not in dont_build:
662 getattr(book, '%s_file' % format_).build_delay()
663 for format_ in constants.EBOOK_FORMATS_WITH_CHILDREN:
664 if format_ not in dont_build:
665 getattr(book, '%s_file' % format_).build_delay()
667 if not settings.NO_SEARCH_INDEX and search_index and findable:
668 tasks.index_book.delay(book.id, book_info=book_info, index_tags=search_index_tags)
670 for child in notify_cover_changed:
671 child.parent_cover_changed()
673 book.update_popularity()
674 tasks.update_references.delay(book.id)
676 cls.published.send(sender=cls, instance=book)
679 def get_master(self):
683 'dramat_wierszowany_l',
684 'dramat_wierszowany_lp',
685 'dramat_wspolczesny', 'liryka_l', 'liryka_lp',
688 from librarian.parser import WLDocument
689 wld = WLDocument.from_file(self.xml_file.path, parse_dublincore=False)
690 root = wld.edoc.getroot()
691 for master in root.iter():
692 if master.tag in master_tags:
695 def update_references(self):
696 from references.models import Entity, Reference
697 master = self.get_master()
699 for i, sec in enumerate(master):
700 for ref in sec.findall('.//ref'):
701 href = ref.attrib.get('href', '')
702 if not href or href in found:
705 entity, created = Entity.objects.get_or_create(
708 ref, created = Reference.objects.get_or_create(
712 ref.first_section = 'sec%d' % (i + 1)
715 Reference.objects.filter(book=self).exclude(entity__uri__in=found).delete()
718 def references(self):
719 return self.reference_set.all().select_related('entity')
723 def repopulate_ancestors(cls):
724 """Fixes the ancestry cache."""
726 cursor = connection.cursor()
727 if connection.vendor == 'postgres':
728 cursor.execute("TRUNCATE catalogue_book_ancestor")
730 WITH RECURSIVE ancestry AS (
731 SELECT book.id, book.parent_id
732 FROM catalogue_book AS book
733 WHERE book.parent_id IS NOT NULL
735 SELECT ancestor.id, book.parent_id
736 FROM ancestry AS ancestor, catalogue_book AS book
737 WHERE ancestor.parent_id = book.id
738 AND book.parent_id IS NOT NULL
740 INSERT INTO catalogue_book_ancestor
741 (from_book_id, to_book_id)
747 cursor.execute("DELETE FROM catalogue_book_ancestor")
748 for b in cls.objects.exclude(parent=None):
750 while parent is not None:
751 b.ancestor.add(parent)
752 parent = parent.parent
754 def clear_cache(self):
755 clear_cached_renders(self.mini_box)
756 clear_cached_renders(self.mini_box_nolink)
758 def cover_info(self, inherit=True):
759 """Returns a dictionary to serve as fallback for BookInfo.
761 For now, the only thing inherited is the cover image.
765 for field in ('cover_url', 'cover_by', 'cover_source'):
766 val = self.get_extra_info_json().get(field)
771 if inherit and need and self.parent is not None:
772 parent_info = self.parent.cover_info()
773 parent_info.update(info)
777 def related_themes(self):
778 return Tag.objects.usage_for_queryset(
779 Fragment.objects.filter(models.Q(book=self) | models.Q(book__ancestor=self)),
780 counts=True).filter(category='theme')
782 def parent_cover_changed(self):
783 """Called when parent book's cover image is changed."""
784 if not self.cover_info(inherit=False):
785 if 'cover' not in app_settings.DONT_BUILD:
786 self.cover.build_delay()
787 self.cover_thumb.build_delay()
788 self.cover_api_thumb.build_delay()
789 self.simple_cover.build_delay()
790 for format_ in constants.EBOOK_FORMATS_WITH_COVERS:
791 if format_ not in app_settings.DONT_BUILD:
792 getattr(self, '%s_file' % format_).build_delay()
793 for child in self.children.all():
794 child.parent_cover_changed()
796 def other_versions(self):
797 """Find other versions (i.e. in other languages) of the book."""
798 return type(self).objects.filter(common_slug=self.common_slug, findable=True).exclude(pk=self.pk)
803 while parent is not None:
804 books.insert(0, parent)
805 parent = parent.parent
808 def pretty_title(self, html_links=False):
809 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
810 books = self.parents() + [self]
811 names.extend([(b.title, b.get_absolute_url()) for b in books])
814 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
816 names = [tag[0] for tag in names]
817 return ', '.join(names)
820 publisher = self.get_extra_info_json()['publisher']
821 if isinstance(publisher, str):
823 elif isinstance(publisher, list):
824 return ', '.join(publisher)
827 def tagged_top_level(cls, tags):
828 """ Returns top-level books tagged with `tags`.
830 It only returns those books which don't have ancestors which are
831 also tagged with those tags.
834 objects = cls.tagged.with_all(tags)
835 return objects.filter(findable=True).exclude(ancestor__in=objects)
838 def book_list(cls, book_filter=None):
839 """Generates a hierarchical listing of all books.
841 Books are optionally filtered with a test function.
846 books = cls.objects.filter(findable=True).order_by('parent_number', 'sort_key').only('title', 'parent', 'slug', 'extra_info')
848 books = books.filter(book_filter).distinct()
850 book_ids = set(b['pk'] for b in books.values("pk").iterator())
851 for book in books.iterator():
852 parent = book.parent_id
853 if parent not in book_ids:
855 books_by_parent.setdefault(parent, []).append(book)
857 for book in books.iterator():
858 books_by_parent.setdefault(book.parent_id, []).append(book)
861 books_by_author = OrderedDict()
862 for tag in Tag.objects.filter(category='author').iterator():
863 books_by_author[tag] = []
865 for book in books_by_parent.get(None, ()):
866 authors = list(book.authors().only('pk'))
868 for author in authors:
869 books_by_author[author].append(book)
873 return books_by_author, orphans, books_by_parent
876 "SP": (1, "szkoła podstawowa"),
877 "SP1": (1, "szkoła podstawowa"),
878 "SP2": (1, "szkoła podstawowa"),
879 "SP3": (1, "szkoła podstawowa"),
880 "P": (1, "szkoła podstawowa"),
881 "G": (2, "gimnazjum"),
886 def audiences_pl(self):
887 audiences = self.get_extra_info_json().get('audiences', [])
888 audiences = sorted(set([self._audiences_pl.get(a, (99, a)) for a in audiences]))
889 return [a[1] for a in audiences]
891 def stage_note(self):
892 stage = self.get_extra_info_json().get('stage')
893 if stage and stage < '0.4':
894 return (_('This work needs modernisation'),
895 reverse('infopage', args=['wymagajace-uwspolczesnienia']))
899 def choose_fragment(self):
900 fragments = self.fragments.order_by()
901 fragments_count = fragments.count()
902 if not fragments_count and self.children.exists():
903 fragments = Fragment.objects.filter(book__ancestor=self).order_by()
904 fragments_count = fragments.count()
906 return fragments[randint(0, fragments_count - 1)]
908 return self.parent.choose_fragment()
912 def fragment_data(self):
913 fragment = self.choose_fragment()
916 'title': fragment.book.pretty_title(),
917 'html': re.sub('</?blockquote[^>]*>', '', fragment.get_short_text()),
922 def update_popularity(self):
923 count = self.tags.filter(category='set').values('user').order_by('user').distinct().count()
925 pop = self.popularity
928 except BookPopularity.DoesNotExist:
929 BookPopularity.objects.create(book=self, count=count)
931 def ridero_link(self):
932 return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
934 def like(self, user):
935 from social.utils import likes, get_set, set_sets
936 if not likes(user, self):
937 tag = get_set(user, '')
938 set_sets(user, self, [tag])
940 def unlike(self, user):
941 from social.utils import likes, set_sets
942 if likes(user, self):
943 set_sets(user, self, [])
945 def full_sort_key(self):
946 return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
948 def cover_color(self):
949 return WLCover.epoch_colors.get(self.get_extra_info_json().get('epoch'), '#000000')
951 @cached_render('catalogue/book_mini_box.html')
957 @cached_render('catalogue/book_mini_box.html')
958 def mini_box_nolink(self):
964 def add_file_fields():
965 for format_ in Book.formats:
966 field_name = "%s_file" % format_
967 # This weird globals() assignment makes Django migrations comfortable.
968 _upload_to = _ebook_upload_to('book/%s/%%s.%s' % (format_, format_))
969 _upload_to.__name__ = '_%s_upload_to' % format_
970 globals()[_upload_to.__name__] = _upload_to
973 format_, _("%s file" % format_.upper()),
974 upload_to=_upload_to,
975 storage=bofh_storage,
979 ).contribute_to_class(Book, field_name)
981 models.CharField(max_length=255, editable=False, default='', db_index=True).contribute_to_class(Book, f'{field_name}_etag')
987 class BookPopularity(models.Model):
988 book = models.OneToOneField(Book, models.CASCADE, related_name='popularity')
989 count = models.IntegerField(default=0, db_index=True)