1 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. 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.apps import apps
12 from django.conf import settings
13 from django.db import connection, models, transaction
14 import django.dispatch
15 from django.contrib.contenttypes.fields import GenericRelation
16 from django.template.loader import render_to_string
17 from django.urls import reverse
18 from django.utils.translation import gettext_lazy as _, get_language
19 from fnpdjango.storage import BofhFileSystemStorage
21 from librarian.cover import WLCover
22 from librarian.html import transform_abstrakt
23 from librarian.builders import builders
24 from newtagging import managers
25 from catalogue import constants
26 from catalogue import fields
27 from catalogue.models import Tag, Fragment, BookMedia
28 from catalogue.utils import create_zip, gallery_url, gallery_path, split_tags, get_random_hash
29 from catalogue.models.tag import prefetched_relations
30 from catalogue import app_settings
31 from wolnelektury.utils import makedirs, cached_render, clear_cached_renders
33 bofh_storage = BofhFileSystemStorage()
36 class Book(models.Model):
37 """Represents a book imported from WL-XML."""
38 title = models.CharField('tytuł', max_length=32767)
39 sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True, editable=False)
40 sort_key_author = models.CharField(
41 'klucz sortowania wg autora', max_length=120, db_index=True, editable=False, default='')
42 slug = models.SlugField('slug', max_length=120, db_index=True, unique=True)
43 common_slug = models.SlugField('wspólny slug', max_length=120, db_index=True)
44 language = models.CharField('kod języka', max_length=3, db_index=True, default=app_settings.DEFAULT_LANGUAGE)
45 description = models.TextField('opis', blank=True)
46 abstract = models.TextField('abstrakt', blank=True)
47 toc = models.TextField('spis treści', blank=True)
48 created_at = models.DateTimeField('data utworzenia', auto_now_add=True, db_index=True)
49 changed_at = models.DateTimeField('data motyfikacji', auto_now=True, db_index=True)
50 parent_number = models.IntegerField('numer w ramach rodzica', default=0)
51 extra_info = models.TextField('dodatkowe informacje', default='{}')
52 gazeta_link = models.CharField(blank=True, max_length=240)
53 wiki_link = models.CharField(blank=True, max_length=240)
54 print_on_demand = models.BooleanField('druk na żądanie', default=False)
55 recommended = models.BooleanField('polecane', default=False)
56 audio_length = models.CharField('długość audio', blank=True, max_length=8)
57 preview = models.BooleanField('prapremiera', default=False)
58 preview_until = models.DateField('prapremiera do', blank=True, null=True)
59 preview_key = models.CharField(max_length=32, blank=True, null=True)
60 findable = models.BooleanField('wyszukiwalna', default=True, db_index=True)
62 # files generated during publication
63 xml_file = fields.XmlField(storage=bofh_storage, with_etag=False)
64 html_file = fields.HtmlField(storage=bofh_storage)
65 fb2_file = fields.Fb2Field(storage=bofh_storage)
66 txt_file = fields.TxtField(storage=bofh_storage)
67 epub_file = fields.EpubField(storage=bofh_storage)
68 mobi_file = fields.MobiField(storage=bofh_storage)
69 pdf_file = fields.PdfField(storage=bofh_storage)
71 cover = fields.CoverField('okładka', storage=bofh_storage)
72 # Cleaner version of cover for thumbs
73 cover_clean = fields.CoverCleanField('czysta okładka')
74 cover_thumb = fields.CoverThumbField('miniatura okładki')
75 cover_api_thumb = fields.CoverApiThumbField(
76 'mniaturka okładki dla aplikacji')
77 simple_cover = fields.SimpleCoverField('okładka dla aplikacji')
78 cover_ebookpoint = fields.CoverEbookpointField(
79 'okładka dla Ebookpoint')
81 ebook_formats = constants.EBOOK_FORMATS
82 formats = ebook_formats + ['html', 'xml']
84 parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True, related_name='children')
85 ancestor = models.ManyToManyField('self', blank=True, editable=False, related_name='descendant', symmetrical=False)
87 cached_author = models.CharField(blank=True, max_length=240, db_index=True)
88 has_audience = models.BooleanField(default=False)
90 objects = models.Manager()
91 tagged = managers.ModelTaggedItemManager(Tag)
92 tags = managers.TagDescriptor(Tag)
93 tag_relations = GenericRelation(Tag.intermediary_table_model)
94 translators = models.ManyToManyField(Tag, blank=True)
96 html_built = django.dispatch.Signal()
97 published = django.dispatch.Signal()
103 class AlreadyExists(Exception):
107 ordering = ('sort_key_author', 'sort_key')
108 verbose_name = 'książka'
109 verbose_name_plural = 'książki'
110 app_label = 'catalogue'
115 def get_extra_info_json(self):
116 return json.loads(self.extra_info or '{}')
118 def get_initial(self):
120 return re.search(r'\w', self.title, re.U).group(0)
121 except AttributeError:
125 return self.tags.filter(category='author')
128 return self.tags.filter(category='epoch')
131 return self.tags.filter(category='genre')
134 return self.tags.filter(category='kind')
136 def tag_unicode(self, category):
137 relations = prefetched_relations(self, category)
139 return ', '.join(rel.tag.name for rel in relations)
141 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
143 def tags_by_category(self):
144 return split_tags(self.tags.exclude(category__in=('set', 'theme')))
146 def author_unicode(self):
147 return self.cached_author
149 def kind_unicode(self):
150 return self.tag_unicode('kind')
152 def epoch_unicode(self):
153 return self.tag_unicode('epoch')
155 def genre_unicode(self):
156 return self.tag_unicode('genre')
158 def translator(self):
159 translators = self.get_extra_info_json().get('translators')
162 if len(translators) > 3:
163 translators = translators[:2]
167 return ', '.join('\xa0'.join(reversed(translator.split(', ', 1))) for translator in translators) + others
169 def cover_source(self):
170 return self.get_extra_info_json().get('cover_source', self.parent.cover_source() if self.parent else '')
174 return self.get_extra_info_json().get('isbn_pdf')
178 return self.get_extra_info_json().get('isbn_epub')
182 return self.get_extra_info_json().get('isbn_mobi')
184 def is_accessible_to(self, user):
187 if not user.is_authenticated:
189 Membership = apps.get_model('club', 'Membership')
190 if Membership.is_active_for(user):
192 Funding = apps.get_model('funding', 'Funding')
193 if Funding.objects.filter(user=user, offer__book=self):
197 def save(self, force_insert=False, force_update=False, **kwargs):
198 from sortify import sortify
200 self.sort_key = sortify(self.title)[:120]
201 self.title = str(self.title) # ???
204 author = self.authors().first().sort_key
205 except AttributeError:
207 self.sort_key_author = author
209 self.cached_author = self.tag_unicode('author')
210 self.has_audience = 'audience' in self.get_extra_info_json()
212 if self.preview and not self.preview_key:
213 self.preview_key = get_random_hash(self.slug)[:32]
215 ret = super(Book, self).save(force_insert, force_update, **kwargs)
219 def get_absolute_url(self):
220 return reverse('book_detail', args=[self.slug])
222 def gallery_path(self):
223 return gallery_path(self.slug)
225 def gallery_url(self):
226 return gallery_url(self.slug)
228 def get_first_text(self):
231 child = self.children.all().order_by('parent_number').first()
232 if child is not None:
233 return child.get_first_text()
235 def get_last_text(self):
238 child = self.children.all().order_by('parent_number').last()
239 if child is not None:
240 return child.get_last_text()
242 def get_prev_text(self):
245 sibling = self.parent.children.filter(parent_number__lt=self.parent_number).order_by('-parent_number').first()
246 if sibling is not None:
247 return sibling.get_last_text()
249 if self.parent.html_file:
252 return self.parent.get_prev_text()
254 def get_next_text(self, inside=True):
256 child = self.children.order_by('parent_number').first()
257 if child is not None:
258 return child.get_first_text()
262 sibling = self.parent.children.filter(parent_number__gt=self.parent_number).order_by('parent_number').first()
263 if sibling is not None:
264 return sibling.get_first_text()
265 return self.parent.get_next_text(inside=False)
267 def get_child_audiobook(self):
268 BookMedia = apps.get_model('catalogue', 'BookMedia')
269 if not BookMedia.objects.filter(book__ancestor=self).exists():
271 for child in self.children.order_by('parent_number').all():
272 if child.has_mp3_file():
274 child_sub = child.get_child_audiobook()
275 if child_sub is not None:
278 def get_siblings(self):
281 return self.parent.children.all().order_by('parent_number')
283 def get_children(self):
284 return self.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)
330 return round(self.xml_file.size / 1000 * 40)
332 def has_media(self, type_):
333 if type_ in Book.formats:
334 return bool(getattr(self, "%s_file" % type_))
336 return self.media.filter(type=type_).exists()
339 return self.has_media('mp3')
341 def get_media(self, type_):
342 if self.has_media(type_):
343 if type_ in Book.formats:
344 return getattr(self, "%s_file" % type_)
346 return self.media.filter(type=type_)
351 return self.get_media("mp3")
354 return self.get_media("odt")
357 return self.get_media("ogg")
360 return self.get_media("daisy")
362 def get_audio_epub(self):
363 return self.get_media("audio.epub")
365 def media_url(self, format_):
366 media = self.get_media(format_)
369 return reverse('embargo_link', kwargs={'key': self.preview_key, 'slug': self.slug, 'format_': format_})
376 return self.media_url('html')
379 return self.media_url('pdf')
382 return self.media_url('epub')
385 return self.media_url('mobi')
388 return self.media_url('txt')
391 return self.media_url('fb2')
394 return self.media_url('xml')
396 def has_description(self):
397 return len(self.description) > 0
398 has_description.short_description = 'opis'
399 has_description.boolean = True
401 def has_mp3_file(self):
402 return self.has_media("mp3")
403 has_mp3_file.short_description = 'MP3'
404 has_mp3_file.boolean = True
406 def has_ogg_file(self):
407 return self.has_media("ogg")
408 has_ogg_file.short_description = 'OGG'
409 has_ogg_file.boolean = True
411 def has_daisy_file(self):
412 return self.has_media("daisy")
413 has_daisy_file.short_description = 'DAISY'
414 has_daisy_file.boolean = True
416 def has_sync_file(self):
417 return settings.FEATURE_SYNCHRO and self.has_media("sync")
420 with self.get_media('sync').first().file.open('r') as f:
421 sync = f.read().split('\n')
422 offset = float(sync[0])
424 for line in sync[1:]:
427 start, end, elid = line.split()
428 items.append([elid, float(start) + offset])
429 return json.dumps(items)
431 def has_audio_epub_file(self):
432 return self.has_media("audio.epub")
435 def media_daisy(self):
436 return self.get_media('daisy')
439 def media_audio_epub(self):
440 return self.get_media('audio.epub')
442 def get_audiobooks(self):
444 for m in self.media.filter(type='ogg').order_by().iterator():
445 ogg_files[m.name] = m
450 for mp3 in self.media.filter(type='mp3').iterator():
451 # ogg files are always from the same project
452 meta = mp3.get_extra_info_json()
453 project = meta.get('project')
456 project = 'CzytamySłuchając'
458 projects.add((project, meta.get('funded_by', '')))
459 total_duration += mp3.duration or 0
463 ogg = ogg_files.get(mp3.name)
466 audiobooks.append(media)
468 projects = sorted(projects)
469 total_duration = '%d:%02d' % (
470 total_duration // 60,
473 return audiobooks, projects, total_duration
475 def wldocument(self, parse_dublincore=True, inherit=True):
476 from catalogue.import_utils import ORMDocProvider
477 from librarian.parser import WLDocument
479 if inherit and self.parent:
480 meta_fallbacks = self.parent.cover_info()
482 meta_fallbacks = None
484 return WLDocument.from_file(
486 provider=ORMDocProvider(self),
487 parse_dublincore=parse_dublincore,
488 meta_fallbacks=meta_fallbacks)
490 def wldocument2(self):
491 from catalogue.import_utils import ORMDocProvider
492 from librarian.document import WLDocument
495 provider=ORMDocProvider(self)
497 doc.meta.update(self.cover_info())
502 def zip_format(format_):
503 def pretty_file_name(book):
504 return "%s/%s.%s" % (
505 book.get_extra_info_json()['author'],
509 field_name = "%s_file" % format_
510 field = getattr(Book, field_name)
511 books = Book.objects.filter(parent=None).exclude(**{field_name: ""}).exclude(preview=True).exclude(findable=False)
512 paths = [(pretty_file_name(b), getattr(b, field_name).path) for b in books.iterator()]
513 return create_zip(paths, field.ZIP)
515 def zip_audiobooks(self, format_):
516 bm = BookMedia.objects.filter(book=self, type=format_)
517 paths = map(lambda bm: (bm.get_nice_filename(), bm.file.path), bm)
520 license = constants.LICENSES.get(
521 m.get_extra_info_json().get('license'), {}
524 licenses.add(license)
525 readme = render_to_string('catalogue/audiobook_zip_readme.txt', {
526 'licenses': licenses,
527 'meta': self.wldocument2().meta,
529 return create_zip(paths, "%s_%s" % (self.slug, format_), {'informacje.txt': readme})
531 def search_index(self, index=None):
532 if not self.findable:
534 from search.index import Index
535 Index.index_book(self)
537 # will make problems in conjunction with paid previews
538 def download_pictures(self, remote_gallery_url):
539 # This is only needed for legacy relative image paths.
540 gallery_path = self.gallery_path()
541 # delete previous files, so we don't include old files in ebooks
542 if os.path.isdir(gallery_path):
543 for filename in os.listdir(gallery_path):
544 file_path = os.path.join(gallery_path, filename)
546 ilustr_elements = list(self.wldocument().edoc.findall('//ilustr'))
548 makedirs(gallery_path)
549 for ilustr in ilustr_elements:
550 ilustr_src = ilustr.get('src')
551 if '/' in ilustr_src:
553 ilustr_path = os.path.join(gallery_path, ilustr_src)
554 urlretrieve('%s/%s' % (remote_gallery_url, ilustr_src), ilustr_path)
556 def load_abstract(self):
557 abstract = self.wldocument(parse_dublincore=False).edoc.getroot().find('.//abstrakt')
558 if abstract is not None:
559 self.abstract = transform_abstrakt(abstract)
566 parser = html.HTMLParser(encoding='utf-8')
567 tree = html.parse(self.html_file.path, parser=parser)
568 toc = tree.find('//div[@id="toc"]/ol')
569 if toc is None or not len(toc):
571 html_link = reverse('book_text', args=[self.slug])
572 for a in toc.findall('.//a'):
573 a.attrib['href'] = html_link + a.attrib['href']
574 self.toc = html.tostring(toc, encoding='unicode')
578 def from_xml_file(cls, xml_file, **kwargs):
579 from django.core.files import File
580 from librarian import dcparser
582 # use librarian to parse meta-data
583 book_info = dcparser.parse(xml_file)
585 if not isinstance(xml_file, File):
586 xml_file = File(open(xml_file))
589 return cls.from_text_and_meta(xml_file, book_info, **kwargs)
594 def from_text_and_meta(cls, raw_file, book_info, overwrite=False, dont_build=None, search_index=True,
595 remote_gallery_url=None, days=0, findable=True, logo=None, logo_mono=None, logo_alt=None):
596 from catalogue import tasks
598 if dont_build is None:
600 dont_build = set.union(set(dont_build), set(app_settings.DONT_BUILD))
602 # check for parts before we do anything
604 if hasattr(book_info, 'parts'):
605 for part_url in book_info.parts:
607 children.append(Book.objects.get(slug=part_url.slug))
608 except Book.DoesNotExist:
609 raise Book.DoesNotExist('Książka "%s" nie istnieje.' % part_url.slug)
612 book_slug = book_info.url.slug
613 if re.search(r'[^a-z0-9-]', book_slug):
614 raise ValueError('Invalid characters in slug')
615 book, created = Book.objects.get_or_create(slug=book_slug)
620 book.preview = bool(days)
622 book.preview_until = date.today() + timedelta(days)
625 raise Book.AlreadyExists('Książka %s już istnieje' % book_slug)
626 # Save shelves for this book
627 book_shelves = list(book.tags.filter(category='set'))
628 old_cover = book.cover_info()
631 book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
633 book.xml_file.set_readable(False)
635 book.findable = findable
636 book.language = book_info.language
637 book.title = book_info.title
638 if book_info.variant_of:
639 book.common_slug = book_info.variant_of.slug
641 book.common_slug = book.slug
642 extra = book_info.to_dict()
646 extra['logo_mono'] = logo_mono
648 extra['logo_alt'] = logo_alt
649 book.extra_info = json.dumps(extra)
654 meta_tags = Tag.tags_from_info(book_info)
656 just_tags = [t for (t, rel) in meta_tags if not rel]
657 book.tags = set(just_tags + book_shelves)
658 book.save() # update sort_key_author
660 book.translators.set([t for (t, rel) in meta_tags if rel == 'translator'])
662 cover_changed = old_cover != book.cover_info()
663 obsolete_children = set(b for b in book.children.all()
664 if b not in children)
665 notify_cover_changed = []
666 for n, child_book in enumerate(children):
667 new_child = child_book.parent != book
668 child_book.parent = book
669 child_book.parent_number = n
671 if new_child or cover_changed:
672 notify_cover_changed.append(child_book)
673 # Disown unfaithful children and let them cope on their own.
674 for child in obsolete_children:
676 child.parent_number = 0
679 notify_cover_changed.append(child)
681 cls.repopulate_ancestors()
682 tasks.update_counters.delay()
684 if remote_gallery_url:
685 book.download_pictures(remote_gallery_url)
687 # No saves beyond this point.
690 if 'cover' not in dont_build:
691 book.cover.build_delay()
692 book.cover_clean.build_delay()
693 book.cover_thumb.build_delay()
694 book.cover_api_thumb.build_delay()
695 book.simple_cover.build_delay()
696 book.cover_ebookpoint.build_delay()
698 # Build HTML and ebooks.
699 book.html_file.build_delay()
701 for format_ in constants.EBOOK_FORMATS_WITHOUT_CHILDREN:
702 if format_ not in dont_build:
703 getattr(book, '%s_file' % format_).build_delay()
704 for format_ in constants.EBOOK_FORMATS_WITH_CHILDREN:
705 if format_ not in dont_build:
706 getattr(book, '%s_file' % format_).build_delay()
708 if not settings.NO_SEARCH_INDEX and search_index and findable:
709 tasks.index_book.delay(book.id)
711 for child in notify_cover_changed:
712 child.parent_cover_changed()
714 book.update_popularity()
715 tasks.update_references.delay(book.id)
717 cls.published.send(sender=cls, instance=book)
720 def update_references(self):
721 Entity = apps.get_model('references', 'Entity')
722 doc = self.wldocument2()
723 doc._compat_assign_section_ids()
724 doc._compat_assign_ordered_ids()
726 for ref_elem in doc.references():
727 uri = ref_elem.attrib.get('href', '')
733 entity, entity_created = Entity.objects.get_or_create(uri=uri)
741 ref, ref_created = entity.reference_set.get_or_create(book=self)
744 ref.occurence_set.all().delete()
745 sec = ref_elem.get_link()
746 m = re.match(r'sec(\d+)', sec)
748 sec = int(m.group(1))
749 snippet = ref_elem.get_snippet()
750 b = builders['html-snippet']()
753 html = b.output().get_bytes().decode('utf-8')
755 ref.occurence_set.create(
759 self.reference_set.exclude(entity__uri__in=refs).delete()
762 def references(self):
763 return self.reference_set.all().select_related('entity')
767 def repopulate_ancestors(cls):
768 """Fixes the ancestry cache."""
770 cursor = connection.cursor()
771 if connection.vendor == 'postgres':
772 cursor.execute("TRUNCATE catalogue_book_ancestor")
774 WITH RECURSIVE ancestry AS (
775 SELECT book.id, book.parent_id
776 FROM catalogue_book AS book
777 WHERE book.parent_id IS NOT NULL
779 SELECT ancestor.id, book.parent_id
780 FROM ancestry AS ancestor, catalogue_book AS book
781 WHERE ancestor.parent_id = book.id
782 AND book.parent_id IS NOT NULL
784 INSERT INTO catalogue_book_ancestor
785 (from_book_id, to_book_id)
791 cursor.execute("DELETE FROM catalogue_book_ancestor")
792 for b in cls.objects.exclude(parent=None):
794 while parent is not None:
795 b.ancestor.add(parent)
796 parent = parent.parent
801 for anc in self.parent.ancestors:
807 def clear_cache(self):
808 clear_cached_renders(self.mini_box)
809 clear_cached_renders(self.mini_box_nolink)
811 def cover_info(self, inherit=True):
812 """Returns a dictionary to serve as fallback for BookInfo.
814 For now, the only thing inherited is the cover image.
818 for field in ('cover_url', 'cover_by', 'cover_source'):
819 val = self.get_extra_info_json().get(field)
824 if inherit and need and self.parent is not None:
825 parent_info = self.parent.cover_info()
826 parent_info.update(info)
830 def related_themes(self):
831 return Tag.objects.usage_for_queryset(
832 Fragment.objects.filter(models.Q(book=self) | models.Q(book__ancestor=self)),
833 counts=True).filter(category='theme').order_by('-count')
835 def parent_cover_changed(self):
836 """Called when parent book's cover image is changed."""
837 if not self.cover_info(inherit=False):
838 if 'cover' not in app_settings.DONT_BUILD:
839 self.cover.build_delay()
840 self.cover_clean.build_delay()
841 self.cover_thumb.build_delay()
842 self.cover_api_thumb.build_delay()
843 self.simple_cover.build_delay()
844 self.cover_ebookpoint.build_delay()
845 for format_ in constants.EBOOK_FORMATS_WITH_COVERS:
846 if format_ not in app_settings.DONT_BUILD:
847 getattr(self, '%s_file' % format_).build_delay()
848 for child in self.children.all():
849 child.parent_cover_changed()
851 def other_versions(self):
852 """Find other versions (i.e. in other languages) of the book."""
853 return type(self).objects.filter(common_slug=self.common_slug, findable=True).exclude(pk=self.pk)
858 while parent is not None:
859 books.insert(0, parent)
860 parent = parent.parent
863 def pretty_title(self, html_links=False):
864 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
865 books = self.parents() + [self]
866 names.extend([(b.title, b.get_absolute_url()) for b in books])
869 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
871 names = [tag[0] for tag in names]
872 return ', '.join(names)
875 publisher = self.get_extra_info_json()['publisher']
876 if isinstance(publisher, str):
878 elif isinstance(publisher, list):
879 return ', '.join(publisher)
882 def tagged_top_level(cls, tags):
883 """ Returns top-level books tagged with `tags`.
885 It only returns those books which don't have ancestors which are
886 also tagged with those tags.
889 objects = cls.tagged.with_all(tags)
890 return objects.filter(findable=True).exclude(ancestor__in=objects)
893 def book_list(cls, book_filter=None):
894 """Generates a hierarchical listing of all books.
896 Books are optionally filtered with a test function.
901 books = cls.objects.filter(findable=True).order_by('parent_number', 'sort_key').only('title', 'parent', 'slug', 'extra_info')
903 books = books.filter(book_filter).distinct()
905 book_ids = set(b['pk'] for b in books.values("pk").iterator())
906 for book in books.iterator():
907 parent = book.parent_id
908 if parent not in book_ids:
910 books_by_parent.setdefault(parent, []).append(book)
912 for book in books.iterator():
913 books_by_parent.setdefault(book.parent_id, []).append(book)
916 books_by_author = OrderedDict()
917 for tag in Tag.objects.filter(category='author').iterator():
918 books_by_author[tag] = []
920 for book in books_by_parent.get(None, ()):
921 authors = list(book.authors().only('pk'))
923 for author in authors:
924 books_by_author[author].append(book)
928 return books_by_author, orphans, books_by_parent
931 "SP": (1, "szkoła podstawowa"),
932 "SP1": (1, "szkoła podstawowa"),
933 "SP2": (1, "szkoła podstawowa"),
934 "SP3": (1, "szkoła podstawowa"),
935 "P": (1, "szkoła podstawowa"),
936 "G": (2, "gimnazjum"),
941 def audiences_pl(self):
942 audiences = self.get_extra_info_json().get('audiences', [])
943 audiences = sorted(set([self._audiences_pl.get(a, (99, a)) for a in audiences]))
944 return [a[1] for a in audiences]
946 def stage_note(self):
947 stage = self.get_extra_info_json().get('stage')
948 if stage and stage < '0.4':
949 return (_('Ten utwór wymaga uwspółcześnienia'),
950 reverse('infopage', args=['wymagajace-uwspolczesnienia']))
954 def choose_fragments(self, number):
955 fragments = self.fragments.order_by()
956 fragments_count = fragments.count()
957 if not fragments_count and self.children.exists():
958 fragments = Fragment.objects.filter(book__ancestor=self).order_by()
959 fragments_count = fragments.count()
961 if fragments_count > number:
962 offset = randint(0, fragments_count - number)
965 return fragments[offset : offset + number]
967 return self.parent.choose_fragments(number)
971 def choose_fragment(self):
972 fragments = self.choose_fragments(1)
978 def fragment_data(self):
979 fragment = self.choose_fragment()
982 'title': fragment.book.pretty_title(),
983 'html': re.sub('</?blockquote[^>]*>', '', fragment.get_short_text()),
988 def update_popularity(self):
989 count = self.tags.filter(category='set').values('user').order_by('user').distinct().count()
991 pop = self.popularity
994 except BookPopularity.DoesNotExist:
995 BookPopularity.objects.create(book=self, count=count)
997 def ridero_link(self):
998 return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
1000 def like(self, user):
1001 from social.utils import likes, get_set, set_sets
1002 if not likes(user, self):
1003 tag = get_set(user, '')
1004 set_sets(user, self, [tag])
1006 def unlike(self, user):
1007 from social.utils import likes, set_sets
1008 if likes(user, self):
1009 set_sets(user, self, [])
1011 def full_sort_key(self):
1012 return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
1014 def cover_color(self):
1015 return WLCover.epoch_colors.get(self.get_extra_info_json().get('epoch'), '#000000')
1017 @cached_render('catalogue/book_mini_box.html')
1023 @cached_render('catalogue/book_mini_box.html')
1024 def mini_box_nolink(self):
1031 class BookPopularity(models.Model):
1032 book = models.OneToOneField(Book, models.CASCADE, related_name='popularity')
1033 count = models.IntegerField(default=0, db_index=True)