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()
271 return self.parent.get_prev_text()
273 def get_next_text(self):
276 sibling = self.parent.children.filter(parent_number__gt=self.parent_number).order_by('parent_number').first()
277 if sibling is not None:
278 return sibling.get_first_text()
279 return self.parent.get_next_text()
281 def get_siblings(self):
284 return self.parent.children.all().order_by('parent_number')
290 def language_code(self):
291 return constants.LANGUAGES_3TO2.get(self.language, self.language)
293 def language_name(self):
294 return dict(settings.LANGUAGES).get(self.language_code(), "")
296 def is_foreign(self):
297 return self.language_code() != settings.LANGUAGE_CODE
299 def set_audio_length(self):
300 length = self.get_audio_length()
302 self.audio_length = self.format_audio_length(length)
306 def format_audio_length(seconds):
308 >>> Book.format_audio_length(1)
310 >>> Book.format_audio_length(3661)
314 minutes = seconds // 60
315 seconds = seconds % 60
316 return '%d:%02d' % (minutes, seconds)
318 hours = seconds // 3600
319 minutes = seconds % 3600 // 60
320 seconds = seconds % 60
321 return '%d:%02d:%02d' % (hours, minutes, seconds)
323 def get_audio_length(self):
325 for media in self.get_mp3() or ():
326 total += app_settings.GET_MP3_LENGTH(media.file.path)
329 def has_media(self, type_):
330 if type_ in Book.formats:
331 return bool(getattr(self, "%s_file" % type_))
333 return self.media.filter(type=type_).exists()
336 return self.has_media('mp3')
338 def get_media(self, type_):
339 if self.has_media(type_):
340 if type_ in Book.formats:
341 return getattr(self, "%s_file" % type_)
343 return self.media.filter(type=type_)
348 return self.get_media("mp3")
351 return self.get_media("odt")
354 return self.get_media("ogg")
357 return self.get_media("daisy")
359 def media_url(self, format_):
360 media = self.get_media(format_)
363 return reverse('embargo_link', kwargs={'key': self.preview_key, 'slug': self.slug, 'format_': format_})
370 return self.media_url('html')
373 return self.media_url('pdf')
376 return self.media_url('epub')
379 return self.media_url('mobi')
382 return self.media_url('txt')
385 return self.media_url('fb2')
388 return self.media_url('xml')
390 def has_description(self):
391 return len(self.description) > 0
392 has_description.short_description = _('description')
393 has_description.boolean = True
395 def has_mp3_file(self):
396 return self.has_media("mp3")
397 has_mp3_file.short_description = 'MP3'
398 has_mp3_file.boolean = True
400 def has_ogg_file(self):
401 return self.has_media("ogg")
402 has_ogg_file.short_description = 'OGG'
403 has_ogg_file.boolean = True
405 def has_daisy_file(self):
406 return self.has_media("daisy")
407 has_daisy_file.short_description = 'DAISY'
408 has_daisy_file.boolean = True
410 def get_audiobooks(self):
412 for m in self.media.filter(type='ogg').order_by().iterator():
413 ogg_files[m.name] = m
417 for mp3 in self.media.filter(type='mp3').iterator():
418 # ogg files are always from the same project
419 meta = mp3.get_extra_info_json()
420 project = meta.get('project')
423 project = 'CzytamySłuchając'
425 projects.add((project, meta.get('funded_by', '')))
429 ogg = ogg_files.get(mp3.name)
432 audiobooks.append(media)
434 projects = sorted(projects)
435 return audiobooks, projects
437 def wldocument(self, parse_dublincore=True, inherit=True):
438 from catalogue.import_utils import ORMDocProvider
439 from librarian.parser import WLDocument
441 if inherit and self.parent:
442 meta_fallbacks = self.parent.cover_info()
444 meta_fallbacks = None
446 return WLDocument.from_file(
448 provider=ORMDocProvider(self),
449 parse_dublincore=parse_dublincore,
450 meta_fallbacks=meta_fallbacks)
452 def wldocument2(self):
453 from catalogue.import_utils import ORMDocProvider
454 from librarian.document import WLDocument
457 provider=ORMDocProvider(self)
459 doc.meta.update(self.cover_info())
464 def zip_format(format_):
465 def pretty_file_name(book):
466 return "%s/%s.%s" % (
467 book.get_extra_info_json()['author'],
471 field_name = "%s_file" % format_
472 books = Book.objects.filter(parent=None).exclude(**{field_name: ""}).exclude(preview=True).exclude(findable=False)
473 paths = [(pretty_file_name(b), getattr(b, field_name).path) for b in books.iterator()]
474 return create_zip(paths, app_settings.FORMAT_ZIPS[format_])
476 def zip_audiobooks(self, format_):
477 bm = BookMedia.objects.filter(book=self, type=format_)
478 paths = map(lambda bm: (None, bm.file.path), bm)
481 license = constants.LICENSES.get(
482 m.get_extra_info_json().get('license'), {}
485 licenses.add(license)
486 readme = render_to_string('catalogue/audiobook_zip_readme.txt', {
487 'licenses': licenses,
489 return create_zip(paths, "%s_%s" % (self.slug, format_), {'informacje.txt': readme})
491 def search_index(self, book_info=None, index=None, index_tags=True, commit=True):
492 if not self.findable:
495 from search.index import Index
498 index.index_book(self, book_info)
503 except Exception as e:
504 index.index.rollback()
507 # will make problems in conjunction with paid previews
508 def download_pictures(self, remote_gallery_url):
509 gallery_path = self.gallery_path()
510 # delete previous files, so we don't include old files in ebooks
511 if os.path.isdir(gallery_path):
512 for filename in os.listdir(gallery_path):
513 file_path = os.path.join(gallery_path, filename)
515 ilustr_elements = list(self.wldocument().edoc.findall('//ilustr'))
517 makedirs(gallery_path)
518 for ilustr in ilustr_elements:
519 ilustr_src = ilustr.get('src')
520 ilustr_path = os.path.join(gallery_path, ilustr_src)
521 urlretrieve('%s/%s' % (remote_gallery_url, ilustr_src), ilustr_path)
523 def load_abstract(self):
524 abstract = self.wldocument(parse_dublincore=False).edoc.getroot().find('.//abstrakt')
525 if abstract is not None:
526 self.abstract = transform_abstrakt(abstract)
531 def from_xml_file(cls, xml_file, **kwargs):
532 from django.core.files import File
533 from librarian import dcparser
535 # use librarian to parse meta-data
536 book_info = dcparser.parse(xml_file)
538 if not isinstance(xml_file, File):
539 xml_file = File(open(xml_file))
542 return cls.from_text_and_meta(xml_file, book_info, **kwargs)
547 def from_text_and_meta(cls, raw_file, book_info, overwrite=False, dont_build=None, search_index=True,
548 search_index_tags=True, remote_gallery_url=None, days=0, findable=True):
549 if dont_build is None:
551 dont_build = set.union(set(dont_build), set(app_settings.DONT_BUILD))
553 # check for parts before we do anything
555 if hasattr(book_info, 'parts'):
556 for part_url in book_info.parts:
558 children.append(Book.objects.get(slug=part_url.slug))
559 except Book.DoesNotExist:
560 raise Book.DoesNotExist(_('Book "%s" does not exist.') % part_url.slug)
563 book_slug = book_info.url.slug
564 if re.search(r'[^a-z0-9-]', book_slug):
565 raise ValueError('Invalid characters in slug')
566 book, created = Book.objects.get_or_create(slug=book_slug)
571 book.preview = bool(days)
573 book.preview_until = date.today() + timedelta(days)
576 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
577 # Save shelves for this book
578 book_shelves = list(book.tags.filter(category='set'))
579 old_cover = book.cover_info()
582 book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
584 book.xml_file.set_readable(False)
586 book.findable = findable
587 book.language = book_info.language
588 book.title = book_info.title
589 if book_info.variant_of:
590 book.common_slug = book_info.variant_of.slug
592 book.common_slug = book.slug
593 book.extra_info = json.dumps(book_info.to_dict())
597 meta_tags = Tag.tags_from_info(book_info)
599 for tag in meta_tags:
600 if not tag.for_books:
604 book.tags = set(meta_tags + book_shelves)
605 book.save() # update sort_key_author
607 cover_changed = old_cover != book.cover_info()
608 obsolete_children = set(b for b in book.children.all()
609 if b not in children)
610 notify_cover_changed = []
611 for n, child_book in enumerate(children):
612 new_child = child_book.parent != book
613 child_book.parent = book
614 child_book.parent_number = n
616 if new_child or cover_changed:
617 notify_cover_changed.append(child_book)
618 # Disown unfaithful children and let them cope on their own.
619 for child in obsolete_children:
621 child.parent_number = 0
624 notify_cover_changed.append(child)
626 cls.repopulate_ancestors()
627 tasks.update_counters.delay()
629 if remote_gallery_url:
630 book.download_pictures(remote_gallery_url)
632 # No saves beyond this point.
635 if 'cover' not in dont_build:
636 book.cover.build_delay()
637 book.cover_thumb.build_delay()
638 book.cover_api_thumb.build_delay()
639 book.simple_cover.build_delay()
640 book.cover_ebookpoint.build_delay()
642 # Build HTML and ebooks.
643 book.html_file.build_delay()
645 for format_ in constants.EBOOK_FORMATS_WITHOUT_CHILDREN:
646 if format_ not in dont_build:
647 getattr(book, '%s_file' % format_).build_delay()
648 for format_ in constants.EBOOK_FORMATS_WITH_CHILDREN:
649 if format_ not in dont_build:
650 getattr(book, '%s_file' % format_).build_delay()
652 if not settings.NO_SEARCH_INDEX and search_index and findable:
653 tasks.index_book.delay(book.id, book_info=book_info, index_tags=search_index_tags)
655 for child in notify_cover_changed:
656 child.parent_cover_changed()
658 book.update_popularity()
659 tasks.update_references.delay(book.id)
661 cls.published.send(sender=cls, instance=book)
664 def get_master(self):
668 'dramat_wierszowany_l',
669 'dramat_wierszowany_lp',
670 'dramat_wspolczesny', 'liryka_l', 'liryka_lp',
673 from librarian.parser import WLDocument
674 wld = WLDocument.from_file(self.xml_file.path, parse_dublincore=False)
675 root = wld.edoc.getroot()
676 for master in root.iter():
677 if master.tag in master_tags:
680 def update_references(self):
681 from references.models import Entity, Reference
682 master = self.get_master()
684 for i, sec in enumerate(master):
685 for ref in sec.findall('.//ref'):
686 href = ref.attrib.get('href', '')
687 if not href or href in found:
690 entity, created = Entity.objects.get_or_create(
693 ref, created = Reference.objects.get_or_create(
697 ref.first_section = 'sec%d' % (i + 1)
700 Reference.objects.filter(book=self).exclude(entity__uri__in=found).delete()
703 def references(self):
704 return self.reference_set.all().select_related('entity')
708 def repopulate_ancestors(cls):
709 """Fixes the ancestry cache."""
711 cursor = connection.cursor()
712 if connection.vendor == 'postgres':
713 cursor.execute("TRUNCATE catalogue_book_ancestor")
715 WITH RECURSIVE ancestry AS (
716 SELECT book.id, book.parent_id
717 FROM catalogue_book AS book
718 WHERE book.parent_id IS NOT NULL
720 SELECT ancestor.id, book.parent_id
721 FROM ancestry AS ancestor, catalogue_book AS book
722 WHERE ancestor.parent_id = book.id
723 AND book.parent_id IS NOT NULL
725 INSERT INTO catalogue_book_ancestor
726 (from_book_id, to_book_id)
732 cursor.execute("DELETE FROM catalogue_book_ancestor")
733 for b in cls.objects.exclude(parent=None):
735 while parent is not None:
736 b.ancestor.add(parent)
737 parent = parent.parent
739 def clear_cache(self):
740 clear_cached_renders(self.mini_box)
741 clear_cached_renders(self.mini_box_nolink)
743 def cover_info(self, inherit=True):
744 """Returns a dictionary to serve as fallback for BookInfo.
746 For now, the only thing inherited is the cover image.
750 for field in ('cover_url', 'cover_by', 'cover_source'):
751 val = self.get_extra_info_json().get(field)
756 if inherit and need and self.parent is not None:
757 parent_info = self.parent.cover_info()
758 parent_info.update(info)
762 def related_themes(self):
763 return Tag.objects.usage_for_queryset(
764 Fragment.objects.filter(models.Q(book=self) | models.Q(book__ancestor=self)),
765 counts=True).filter(category='theme')
767 def parent_cover_changed(self):
768 """Called when parent book's cover image is changed."""
769 if not self.cover_info(inherit=False):
770 if 'cover' not in app_settings.DONT_BUILD:
771 self.cover.build_delay()
772 self.cover_thumb.build_delay()
773 self.cover_api_thumb.build_delay()
774 self.simple_cover.build_delay()
775 for format_ in constants.EBOOK_FORMATS_WITH_COVERS:
776 if format_ not in app_settings.DONT_BUILD:
777 getattr(self, '%s_file' % format_).build_delay()
778 for child in self.children.all():
779 child.parent_cover_changed()
781 def other_versions(self):
782 """Find other versions (i.e. in other languages) of the book."""
783 return type(self).objects.filter(common_slug=self.common_slug, findable=True).exclude(pk=self.pk)
788 while parent is not None:
789 books.insert(0, parent)
790 parent = parent.parent
793 def pretty_title(self, html_links=False):
794 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
795 books = self.parents() + [self]
796 names.extend([(b.title, b.get_absolute_url()) for b in books])
799 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
801 names = [tag[0] for tag in names]
802 return ', '.join(names)
805 publisher = self.get_extra_info_json()['publisher']
806 if isinstance(publisher, str):
808 elif isinstance(publisher, list):
809 return ', '.join(publisher)
812 def tagged_top_level(cls, tags):
813 """ Returns top-level books tagged with `tags`.
815 It only returns those books which don't have ancestors which are
816 also tagged with those tags.
819 objects = cls.tagged.with_all(tags)
820 return objects.filter(findable=True).exclude(ancestor__in=objects)
823 def book_list(cls, book_filter=None):
824 """Generates a hierarchical listing of all books.
826 Books are optionally filtered with a test function.
831 books = cls.objects.filter(findable=True).order_by('parent_number', 'sort_key').only('title', 'parent', 'slug', 'extra_info')
833 books = books.filter(book_filter).distinct()
835 book_ids = set(b['pk'] for b in books.values("pk").iterator())
836 for book in books.iterator():
837 parent = book.parent_id
838 if parent not in book_ids:
840 books_by_parent.setdefault(parent, []).append(book)
842 for book in books.iterator():
843 books_by_parent.setdefault(book.parent_id, []).append(book)
846 books_by_author = OrderedDict()
847 for tag in Tag.objects.filter(category='author').iterator():
848 books_by_author[tag] = []
850 for book in books_by_parent.get(None, ()):
851 authors = list(book.authors().only('pk'))
853 for author in authors:
854 books_by_author[author].append(book)
858 return books_by_author, orphans, books_by_parent
861 "SP": (1, "szkoła podstawowa"),
862 "SP1": (1, "szkoła podstawowa"),
863 "SP2": (1, "szkoła podstawowa"),
864 "SP3": (1, "szkoła podstawowa"),
865 "P": (1, "szkoła podstawowa"),
866 "G": (2, "gimnazjum"),
871 def audiences_pl(self):
872 audiences = self.get_extra_info_json().get('audiences', [])
873 audiences = sorted(set([self._audiences_pl.get(a, (99, a)) for a in audiences]))
874 return [a[1] for a in audiences]
876 def stage_note(self):
877 stage = self.get_extra_info_json().get('stage')
878 if stage and stage < '0.4':
879 return (_('This work needs modernisation'),
880 reverse('infopage', args=['wymagajace-uwspolczesnienia']))
884 def choose_fragment(self):
885 fragments = self.fragments.order_by()
886 fragments_count = fragments.count()
887 if not fragments_count and self.children.exists():
888 fragments = Fragment.objects.filter(book__ancestor=self).order_by()
889 fragments_count = fragments.count()
891 return fragments[randint(0, fragments_count - 1)]
893 return self.parent.choose_fragment()
897 def fragment_data(self):
898 fragment = self.choose_fragment()
901 'title': fragment.book.pretty_title(),
902 'html': re.sub('</?blockquote[^>]*>', '', fragment.get_short_text()),
907 def update_popularity(self):
908 count = self.tags.filter(category='set').values('user').order_by('user').distinct().count()
910 pop = self.popularity
913 except BookPopularity.DoesNotExist:
914 BookPopularity.objects.create(book=self, count=count)
916 def ridero_link(self):
917 return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
919 def like(self, user):
920 from social.utils import likes, get_set, set_sets
921 if not likes(user, self):
922 tag = get_set(user, '')
923 set_sets(user, self, [tag])
925 def unlike(self, user):
926 from social.utils import likes, set_sets
927 if likes(user, self):
928 set_sets(user, self, [])
930 def full_sort_key(self):
931 return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
933 def cover_color(self):
934 return WLCover.epoch_colors.get(self.get_extra_info_json().get('epoch'), '#000000')
936 @cached_render('catalogue/book_mini_box.html')
942 @cached_render('catalogue/book_mini_box.html')
943 def mini_box_nolink(self):
949 def add_file_fields():
950 for format_ in Book.formats:
951 field_name = "%s_file" % format_
952 # This weird globals() assignment makes Django migrations comfortable.
953 _upload_to = _ebook_upload_to('book/%s/%%s.%s' % (format_, format_))
954 _upload_to.__name__ = '_%s_upload_to' % format_
955 globals()[_upload_to.__name__] = _upload_to
958 format_, _("%s file" % format_.upper()),
959 upload_to=_upload_to,
960 storage=bofh_storage,
964 ).contribute_to_class(Book, field_name)
966 models.CharField(max_length=255, editable=False, default='', db_index=True).contribute_to_class(Book, f'{field_name}_etag')
972 class BookPopularity(models.Model):
973 book = models.OneToOneField(Book, models.CASCADE, related_name='popularity')
974 count = models.IntegerField(default=0, db_index=True)