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
411 def media_daisy(self):
412 return self.get_media('daisy')
414 def get_audiobooks(self):
416 for m in self.media.filter(type='ogg').order_by().iterator():
417 ogg_files[m.name] = m
421 for mp3 in self.media.filter(type='mp3').iterator():
422 # ogg files are always from the same project
423 meta = mp3.get_extra_info_json()
424 project = meta.get('project')
427 project = 'CzytamySłuchając'
429 projects.add((project, meta.get('funded_by', '')))
433 ogg = ogg_files.get(mp3.name)
436 audiobooks.append(media)
438 projects = sorted(projects)
439 return audiobooks, projects
441 def wldocument(self, parse_dublincore=True, inherit=True):
442 from catalogue.import_utils import ORMDocProvider
443 from librarian.parser import WLDocument
445 if inherit and self.parent:
446 meta_fallbacks = self.parent.cover_info()
448 meta_fallbacks = None
450 return WLDocument.from_file(
452 provider=ORMDocProvider(self),
453 parse_dublincore=parse_dublincore,
454 meta_fallbacks=meta_fallbacks)
456 def wldocument2(self):
457 from catalogue.import_utils import ORMDocProvider
458 from librarian.document import WLDocument
461 provider=ORMDocProvider(self)
463 doc.meta.update(self.cover_info())
468 def zip_format(format_):
469 def pretty_file_name(book):
470 return "%s/%s.%s" % (
471 book.get_extra_info_json()['author'],
475 field_name = "%s_file" % format_
476 books = Book.objects.filter(parent=None).exclude(**{field_name: ""}).exclude(preview=True).exclude(findable=False)
477 paths = [(pretty_file_name(b), getattr(b, field_name).path) for b in books.iterator()]
478 return create_zip(paths, app_settings.FORMAT_ZIPS[format_])
480 def zip_audiobooks(self, format_):
481 bm = BookMedia.objects.filter(book=self, type=format_)
482 paths = map(lambda bm: (bm.get_nice_filename(), bm.file.path), bm)
485 license = constants.LICENSES.get(
486 m.get_extra_info_json().get('license'), {}
489 licenses.add(license)
490 readme = render_to_string('catalogue/audiobook_zip_readme.txt', {
491 'licenses': licenses,
493 return create_zip(paths, "%s_%s" % (self.slug, format_), {'informacje.txt': readme})
495 def search_index(self, book_info=None, index=None, index_tags=True, commit=True):
496 if not self.findable:
499 from search.index import Index
502 index.index_book(self, book_info)
507 except Exception as e:
508 index.index.rollback()
511 # will make problems in conjunction with paid previews
512 def download_pictures(self, remote_gallery_url):
513 gallery_path = self.gallery_path()
514 # delete previous files, so we don't include old files in ebooks
515 if os.path.isdir(gallery_path):
516 for filename in os.listdir(gallery_path):
517 file_path = os.path.join(gallery_path, filename)
519 ilustr_elements = list(self.wldocument().edoc.findall('//ilustr'))
521 makedirs(gallery_path)
522 for ilustr in ilustr_elements:
523 ilustr_src = ilustr.get('src')
524 ilustr_path = os.path.join(gallery_path, ilustr_src)
525 urlretrieve('%s/%s' % (remote_gallery_url, ilustr_src), ilustr_path)
527 def load_abstract(self):
528 abstract = self.wldocument(parse_dublincore=False).edoc.getroot().find('.//abstrakt')
529 if abstract is not None:
530 self.abstract = transform_abstrakt(abstract)
535 def from_xml_file(cls, xml_file, **kwargs):
536 from django.core.files import File
537 from librarian import dcparser
539 # use librarian to parse meta-data
540 book_info = dcparser.parse(xml_file)
542 if not isinstance(xml_file, File):
543 xml_file = File(open(xml_file))
546 return cls.from_text_and_meta(xml_file, book_info, **kwargs)
551 def from_text_and_meta(cls, raw_file, book_info, overwrite=False, dont_build=None, search_index=True,
552 search_index_tags=True, remote_gallery_url=None, days=0, findable=True):
553 if dont_build is None:
555 dont_build = set.union(set(dont_build), set(app_settings.DONT_BUILD))
557 # check for parts before we do anything
559 if hasattr(book_info, 'parts'):
560 for part_url in book_info.parts:
562 children.append(Book.objects.get(slug=part_url.slug))
563 except Book.DoesNotExist:
564 raise Book.DoesNotExist(_('Book "%s" does not exist.') % part_url.slug)
567 book_slug = book_info.url.slug
568 if re.search(r'[^a-z0-9-]', book_slug):
569 raise ValueError('Invalid characters in slug')
570 book, created = Book.objects.get_or_create(slug=book_slug)
575 book.preview = bool(days)
577 book.preview_until = date.today() + timedelta(days)
580 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
581 # Save shelves for this book
582 book_shelves = list(book.tags.filter(category='set'))
583 old_cover = book.cover_info()
586 book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
588 book.xml_file.set_readable(False)
590 book.findable = findable
591 book.language = book_info.language
592 book.title = book_info.title
593 if book_info.variant_of:
594 book.common_slug = book_info.variant_of.slug
596 book.common_slug = book.slug
597 book.extra_info = json.dumps(book_info.to_dict())
601 meta_tags = Tag.tags_from_info(book_info)
603 for tag in meta_tags:
604 if not tag.for_books:
608 book.tags = set(meta_tags + book_shelves)
609 book.save() # update sort_key_author
611 cover_changed = old_cover != book.cover_info()
612 obsolete_children = set(b for b in book.children.all()
613 if b not in children)
614 notify_cover_changed = []
615 for n, child_book in enumerate(children):
616 new_child = child_book.parent != book
617 child_book.parent = book
618 child_book.parent_number = n
620 if new_child or cover_changed:
621 notify_cover_changed.append(child_book)
622 # Disown unfaithful children and let them cope on their own.
623 for child in obsolete_children:
625 child.parent_number = 0
628 notify_cover_changed.append(child)
630 cls.repopulate_ancestors()
631 tasks.update_counters.delay()
633 if remote_gallery_url:
634 book.download_pictures(remote_gallery_url)
636 # No saves beyond this point.
639 if 'cover' not in dont_build:
640 book.cover.build_delay()
641 book.cover_thumb.build_delay()
642 book.cover_api_thumb.build_delay()
643 book.simple_cover.build_delay()
644 book.cover_ebookpoint.build_delay()
646 # Build HTML and ebooks.
647 book.html_file.build_delay()
649 for format_ in constants.EBOOK_FORMATS_WITHOUT_CHILDREN:
650 if format_ not in dont_build:
651 getattr(book, '%s_file' % format_).build_delay()
652 for format_ in constants.EBOOK_FORMATS_WITH_CHILDREN:
653 if format_ not in dont_build:
654 getattr(book, '%s_file' % format_).build_delay()
656 if not settings.NO_SEARCH_INDEX and search_index and findable:
657 tasks.index_book.delay(book.id, book_info=book_info, index_tags=search_index_tags)
659 for child in notify_cover_changed:
660 child.parent_cover_changed()
662 book.update_popularity()
663 tasks.update_references.delay(book.id)
665 cls.published.send(sender=cls, instance=book)
668 def get_master(self):
672 'dramat_wierszowany_l',
673 'dramat_wierszowany_lp',
674 'dramat_wspolczesny', 'liryka_l', 'liryka_lp',
677 from librarian.parser import WLDocument
678 wld = WLDocument.from_file(self.xml_file.path, parse_dublincore=False)
679 root = wld.edoc.getroot()
680 for master in root.iter():
681 if master.tag in master_tags:
684 def update_references(self):
685 from references.models import Entity, Reference
686 master = self.get_master()
688 for i, sec in enumerate(master):
689 for ref in sec.findall('.//ref'):
690 href = ref.attrib.get('href', '')
691 if not href or href in found:
694 entity, created = Entity.objects.get_or_create(
697 ref, created = Reference.objects.get_or_create(
701 ref.first_section = 'sec%d' % (i + 1)
704 Reference.objects.filter(book=self).exclude(entity__uri__in=found).delete()
707 def references(self):
708 return self.reference_set.all().select_related('entity')
712 def repopulate_ancestors(cls):
713 """Fixes the ancestry cache."""
715 cursor = connection.cursor()
716 if connection.vendor == 'postgres':
717 cursor.execute("TRUNCATE catalogue_book_ancestor")
719 WITH RECURSIVE ancestry AS (
720 SELECT book.id, book.parent_id
721 FROM catalogue_book AS book
722 WHERE book.parent_id IS NOT NULL
724 SELECT ancestor.id, book.parent_id
725 FROM ancestry AS ancestor, catalogue_book AS book
726 WHERE ancestor.parent_id = book.id
727 AND book.parent_id IS NOT NULL
729 INSERT INTO catalogue_book_ancestor
730 (from_book_id, to_book_id)
736 cursor.execute("DELETE FROM catalogue_book_ancestor")
737 for b in cls.objects.exclude(parent=None):
739 while parent is not None:
740 b.ancestor.add(parent)
741 parent = parent.parent
743 def clear_cache(self):
744 clear_cached_renders(self.mini_box)
745 clear_cached_renders(self.mini_box_nolink)
747 def cover_info(self, inherit=True):
748 """Returns a dictionary to serve as fallback for BookInfo.
750 For now, the only thing inherited is the cover image.
754 for field in ('cover_url', 'cover_by', 'cover_source'):
755 val = self.get_extra_info_json().get(field)
760 if inherit and need and self.parent is not None:
761 parent_info = self.parent.cover_info()
762 parent_info.update(info)
766 def related_themes(self):
767 return Tag.objects.usage_for_queryset(
768 Fragment.objects.filter(models.Q(book=self) | models.Q(book__ancestor=self)),
769 counts=True).filter(category='theme')
771 def parent_cover_changed(self):
772 """Called when parent book's cover image is changed."""
773 if not self.cover_info(inherit=False):
774 if 'cover' not in app_settings.DONT_BUILD:
775 self.cover.build_delay()
776 self.cover_thumb.build_delay()
777 self.cover_api_thumb.build_delay()
778 self.simple_cover.build_delay()
779 for format_ in constants.EBOOK_FORMATS_WITH_COVERS:
780 if format_ not in app_settings.DONT_BUILD:
781 getattr(self, '%s_file' % format_).build_delay()
782 for child in self.children.all():
783 child.parent_cover_changed()
785 def other_versions(self):
786 """Find other versions (i.e. in other languages) of the book."""
787 return type(self).objects.filter(common_slug=self.common_slug, findable=True).exclude(pk=self.pk)
792 while parent is not None:
793 books.insert(0, parent)
794 parent = parent.parent
797 def pretty_title(self, html_links=False):
798 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
799 books = self.parents() + [self]
800 names.extend([(b.title, b.get_absolute_url()) for b in books])
803 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
805 names = [tag[0] for tag in names]
806 return ', '.join(names)
809 publisher = self.get_extra_info_json()['publisher']
810 if isinstance(publisher, str):
812 elif isinstance(publisher, list):
813 return ', '.join(publisher)
816 def tagged_top_level(cls, tags):
817 """ Returns top-level books tagged with `tags`.
819 It only returns those books which don't have ancestors which are
820 also tagged with those tags.
823 objects = cls.tagged.with_all(tags)
824 return objects.filter(findable=True).exclude(ancestor__in=objects)
827 def book_list(cls, book_filter=None):
828 """Generates a hierarchical listing of all books.
830 Books are optionally filtered with a test function.
835 books = cls.objects.filter(findable=True).order_by('parent_number', 'sort_key').only('title', 'parent', 'slug', 'extra_info')
837 books = books.filter(book_filter).distinct()
839 book_ids = set(b['pk'] for b in books.values("pk").iterator())
840 for book in books.iterator():
841 parent = book.parent_id
842 if parent not in book_ids:
844 books_by_parent.setdefault(parent, []).append(book)
846 for book in books.iterator():
847 books_by_parent.setdefault(book.parent_id, []).append(book)
850 books_by_author = OrderedDict()
851 for tag in Tag.objects.filter(category='author').iterator():
852 books_by_author[tag] = []
854 for book in books_by_parent.get(None, ()):
855 authors = list(book.authors().only('pk'))
857 for author in authors:
858 books_by_author[author].append(book)
862 return books_by_author, orphans, books_by_parent
865 "SP": (1, "szkoła podstawowa"),
866 "SP1": (1, "szkoła podstawowa"),
867 "SP2": (1, "szkoła podstawowa"),
868 "SP3": (1, "szkoła podstawowa"),
869 "P": (1, "szkoła podstawowa"),
870 "G": (2, "gimnazjum"),
875 def audiences_pl(self):
876 audiences = self.get_extra_info_json().get('audiences', [])
877 audiences = sorted(set([self._audiences_pl.get(a, (99, a)) for a in audiences]))
878 return [a[1] for a in audiences]
880 def stage_note(self):
881 stage = self.get_extra_info_json().get('stage')
882 if stage and stage < '0.4':
883 return (_('This work needs modernisation'),
884 reverse('infopage', args=['wymagajace-uwspolczesnienia']))
888 def choose_fragment(self):
889 fragments = self.fragments.order_by()
890 fragments_count = fragments.count()
891 if not fragments_count and self.children.exists():
892 fragments = Fragment.objects.filter(book__ancestor=self).order_by()
893 fragments_count = fragments.count()
895 return fragments[randint(0, fragments_count - 1)]
897 return self.parent.choose_fragment()
901 def fragment_data(self):
902 fragment = self.choose_fragment()
905 'title': fragment.book.pretty_title(),
906 'html': re.sub('</?blockquote[^>]*>', '', fragment.get_short_text()),
911 def update_popularity(self):
912 count = self.tags.filter(category='set').values('user').order_by('user').distinct().count()
914 pop = self.popularity
917 except BookPopularity.DoesNotExist:
918 BookPopularity.objects.create(book=self, count=count)
920 def ridero_link(self):
921 return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
923 def like(self, user):
924 from social.utils import likes, get_set, set_sets
925 if not likes(user, self):
926 tag = get_set(user, '')
927 set_sets(user, self, [tag])
929 def unlike(self, user):
930 from social.utils import likes, set_sets
931 if likes(user, self):
932 set_sets(user, self, [])
934 def full_sort_key(self):
935 return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
937 def cover_color(self):
938 return WLCover.epoch_colors.get(self.get_extra_info_json().get('epoch'), '#000000')
940 @cached_render('catalogue/book_mini_box.html')
946 @cached_render('catalogue/book_mini_box.html')
947 def mini_box_nolink(self):
953 def add_file_fields():
954 for format_ in Book.formats:
955 field_name = "%s_file" % format_
956 # This weird globals() assignment makes Django migrations comfortable.
957 _upload_to = _ebook_upload_to('book/%s/%%s.%s' % (format_, format_))
958 _upload_to.__name__ = '_%s_upload_to' % format_
959 globals()[_upload_to.__name__] = _upload_to
962 format_, _("%s file" % format_.upper()),
963 upload_to=_upload_to,
964 storage=bofh_storage,
968 ).contribute_to_class(Book, field_name)
970 models.CharField(max_length=255, editable=False, default='', db_index=True).contribute_to_class(Book, f'{field_name}_etag')
976 class BookPopularity(models.Model):
977 book = models.OneToOneField(Book, models.CASCADE, related_name='popularity')
978 count = models.IntegerField(default=0, db_index=True)