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.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 newtagging import managers
24 from catalogue import constants
25 from catalogue import fields
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 wolnelektury.utils import makedirs, cached_render, clear_cached_renders
32 bofh_storage = BofhFileSystemStorage()
35 class Book(models.Model):
36 """Represents a book imported from WL-XML."""
37 title = models.CharField(_('title'), max_length=32767)
38 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
39 sort_key_author = models.CharField(
40 _('sort key by author'), max_length=120, db_index=True, editable=False, default='')
41 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
42 common_slug = models.SlugField(_('slug'), max_length=120, db_index=True)
43 language = models.CharField(_('language code'), max_length=3, db_index=True, default=app_settings.DEFAULT_LANGUAGE)
44 description = models.TextField(_('description'), blank=True)
45 abstract = models.TextField(_('abstract'), blank=True)
46 toc = models.TextField(_('toc'), blank=True)
47 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
48 changed_at = models.DateTimeField(_('change date'), auto_now=True, db_index=True)
49 parent_number = models.IntegerField(_('parent number'), default=0)
50 extra_info = models.TextField(_('extra information'), default='{}')
51 gazeta_link = models.CharField(blank=True, max_length=240)
52 wiki_link = models.CharField(blank=True, max_length=240)
53 print_on_demand = models.BooleanField(_('print on demand'), default=False)
54 recommended = models.BooleanField(_('recommended'), default=False)
55 audio_length = models.CharField(_('audio length'), blank=True, max_length=8)
56 preview = models.BooleanField(_('preview'), default=False)
57 preview_until = models.DateField(_('preview until'), blank=True, null=True)
58 preview_key = models.CharField(max_length=32, blank=True, null=True)
59 findable = models.BooleanField(_('findable'), default=True, db_index=True)
61 # files generated during publication
62 xml_file = fields.XmlField(storage=bofh_storage, with_etag=False)
63 html_file = fields.HtmlField(storage=bofh_storage)
64 fb2_file = fields.Fb2Field(storage=bofh_storage)
65 txt_file = fields.TxtField(storage=bofh_storage)
66 epub_file = fields.EpubField(storage=bofh_storage)
67 mobi_file = fields.MobiField(storage=bofh_storage)
68 pdf_file = fields.PdfField(storage=bofh_storage)
70 cover = fields.CoverField(_('cover'), storage=bofh_storage)
71 # Cleaner version of cover for thumbs
72 cover_clean = fields.CoverCleanField(_('clean cover'))
73 cover_thumb = fields.CoverThumbField(_('cover thumbnail'))
74 cover_api_thumb = fields.CoverApiThumbField(
75 _('cover thumbnail for mobile app'))
76 simple_cover = fields.SimpleCoverField(_('cover for mobile app'))
77 cover_ebookpoint = fields.CoverEbookpointField(
78 _('cover for Ebookpoint'))
80 ebook_formats = constants.EBOOK_FORMATS
81 formats = ebook_formats + ['html', 'xml']
83 parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True, related_name='children')
84 ancestor = models.ManyToManyField('self', blank=True, editable=False, related_name='descendant', symmetrical=False)
86 cached_author = models.CharField(blank=True, max_length=240, db_index=True)
87 has_audience = models.BooleanField(default=False)
89 objects = models.Manager()
90 tagged = managers.ModelTaggedItemManager(Tag)
91 tags = managers.TagDescriptor(Tag)
92 tag_relations = GenericRelation(Tag.intermediary_table_model)
94 html_built = django.dispatch.Signal()
95 published = django.dispatch.Signal()
101 class AlreadyExists(Exception):
105 ordering = ('sort_key_author', 'sort_key')
106 verbose_name = _('book')
107 verbose_name_plural = _('books')
108 app_label = 'catalogue'
113 def get_extra_info_json(self):
114 return json.loads(self.extra_info or '{}')
116 def get_initial(self):
118 return re.search(r'\w', self.title, re.U).group(0)
119 except AttributeError:
123 return self.tags.filter(category='author')
126 return self.tags.filter(category='epoch')
129 return self.tags.filter(category='genre')
132 return self.tags.filter(category='kind')
134 def tag_unicode(self, category):
135 relations = prefetched_relations(self, category)
137 return ', '.join(rel.tag.name for rel in relations)
139 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
141 def tags_by_category(self):
142 return split_tags(self.tags.exclude(category__in=('set', 'theme')))
144 def author_unicode(self):
145 return self.cached_author
147 def kind_unicode(self):
148 return self.tag_unicode('kind')
150 def epoch_unicode(self):
151 return self.tag_unicode('epoch')
153 def genre_unicode(self):
154 return self.tag_unicode('genre')
156 def translators(self):
157 translators = self.get_extra_info_json().get('translators') or []
159 '\xa0'.join(reversed(translator.split(', ', 1))) for translator in translators
162 def translator(self):
163 translators = self.get_extra_info_json().get('translators')
166 if len(translators) > 3:
167 translators = translators[:2]
171 return ', '.join('\xa0'.join(reversed(translator.split(', ', 1))) for translator in translators) + others
173 def cover_source(self):
174 return self.get_extra_info_json().get('cover_source', self.parent.cover_source() if self.parent else '')
178 return self.get_extra_info_json().get('isbn_pdf')
182 return self.get_extra_info_json().get('isbn_epub')
186 return self.get_extra_info_json().get('isbn_mobi')
188 def is_accessible_to(self, user):
191 if not user.is_authenticated:
193 Membership = apps.get_model('club', 'Membership')
194 if Membership.is_active_for(user):
196 Funding = apps.get_model('funding', 'Funding')
197 if Funding.objects.filter(user=user, offer__book=self):
201 def save(self, force_insert=False, force_update=False, **kwargs):
202 from sortify import sortify
204 self.sort_key = sortify(self.title)[:120]
205 self.title = str(self.title) # ???
208 author = self.authors().first().sort_key
209 except AttributeError:
211 self.sort_key_author = author
213 self.cached_author = self.tag_unicode('author')
214 self.has_audience = 'audience' in self.get_extra_info_json()
216 if self.preview and not self.preview_key:
217 self.preview_key = get_random_hash(self.slug)[:32]
219 ret = super(Book, self).save(force_insert, force_update, **kwargs)
223 def get_absolute_url(self):
224 return reverse('book_detail', args=[self.slug])
226 def gallery_path(self):
227 return gallery_path(self.slug)
229 def gallery_url(self):
230 return gallery_url(self.slug)
232 def get_first_text(self):
235 child = self.children.all().order_by('parent_number').first()
236 if child is not None:
237 return child.get_first_text()
239 def get_last_text(self):
242 child = self.children.all().order_by('parent_number').last()
243 if child is not None:
244 return child.get_last_text()
246 def get_prev_text(self):
249 sibling = self.parent.children.filter(parent_number__lt=self.parent_number).order_by('-parent_number').first()
250 if sibling is not None:
251 return sibling.get_last_text()
253 if self.parent.html_file:
256 return self.parent.get_prev_text()
258 def get_next_text(self, inside=True):
260 child = self.children.order_by('parent_number').first()
261 if child is not None:
262 return child.get_first_text()
266 sibling = self.parent.children.filter(parent_number__gt=self.parent_number).order_by('parent_number').first()
267 if sibling is not None:
268 return sibling.get_first_text()
269 return self.parent.get_next_text(inside=False)
271 def get_child_audiobook(self):
272 BookMedia = apps.get_model('catalogue', 'BookMedia')
273 if not BookMedia.objects.filter(book__ancestor=self).exists():
275 for child in self.children.order_by('parent_number').all():
276 if child.has_mp3_file():
278 child_sub = child.get_child_audiobook()
279 if child_sub is not None:
282 def get_siblings(self):
285 return self.parent.children.all().order_by('parent_number')
287 def get_children(self):
288 return self.children.all().order_by('parent_number')
294 def language_code(self):
295 return constants.LANGUAGES_3TO2.get(self.language, self.language)
297 def language_name(self):
298 return dict(settings.LANGUAGES).get(self.language_code(), "")
300 def is_foreign(self):
301 return self.language_code() != settings.LANGUAGE_CODE
303 def set_audio_length(self):
304 length = self.get_audio_length()
306 self.audio_length = self.format_audio_length(length)
310 def format_audio_length(seconds):
312 >>> Book.format_audio_length(1)
314 >>> Book.format_audio_length(3661)
318 minutes = seconds // 60
319 seconds = seconds % 60
320 return '%d:%02d' % (minutes, seconds)
322 hours = seconds // 3600
323 minutes = seconds % 3600 // 60
324 seconds = seconds % 60
325 return '%d:%02d:%02d' % (hours, minutes, seconds)
327 def get_audio_length(self):
329 for media in self.get_mp3() or ():
330 total += app_settings.GET_MP3_LENGTH(media.file.path)
333 def has_media(self, type_):
334 if type_ in Book.formats:
335 return bool(getattr(self, "%s_file" % type_))
337 return self.media.filter(type=type_).exists()
340 return self.has_media('mp3')
342 def get_media(self, type_):
343 if self.has_media(type_):
344 if type_ in Book.formats:
345 return getattr(self, "%s_file" % type_)
347 return self.media.filter(type=type_)
352 return self.get_media("mp3")
355 return self.get_media("odt")
358 return self.get_media("ogg")
361 return self.get_media("daisy")
363 def get_audio_epub(self):
364 return self.get_media("audio.epub")
366 def media_url(self, format_):
367 media = self.get_media(format_)
370 return reverse('embargo_link', kwargs={'key': self.preview_key, 'slug': self.slug, 'format_': format_})
377 return self.media_url('html')
380 return self.media_url('pdf')
383 return self.media_url('epub')
386 return self.media_url('mobi')
389 return self.media_url('txt')
392 return self.media_url('fb2')
395 return self.media_url('xml')
397 def has_description(self):
398 return len(self.description) > 0
399 has_description.short_description = _('description')
400 has_description.boolean = True
402 def has_mp3_file(self):
403 return self.has_media("mp3")
404 has_mp3_file.short_description = 'MP3'
405 has_mp3_file.boolean = True
407 def has_ogg_file(self):
408 return self.has_media("ogg")
409 has_ogg_file.short_description = 'OGG'
410 has_ogg_file.boolean = True
412 def has_daisy_file(self):
413 return self.has_media("daisy")
414 has_daisy_file.short_description = 'DAISY'
415 has_daisy_file.boolean = True
417 def has_sync_file(self):
418 return self.has_media("sync")
421 with self.get_media('sync').first().file.open('r') as f:
422 sync = f.read().split('\n')
423 offset = float(sync[0])
425 for line in sync[1:]:
428 start, end, elid = line.split()
429 items.append([elid, float(start) + offset])
430 return json.dumps(items)
432 def has_audio_epub_file(self):
433 return self.has_media("audio.epub")
436 def media_daisy(self):
437 return self.get_media('daisy')
440 def media_audio_epub(self):
441 return self.get_media('audio.epub')
443 def get_audiobooks(self):
445 for m in self.media.filter(type='ogg').order_by().iterator():
446 ogg_files[m.name] = m
451 for mp3 in self.media.filter(type='mp3').iterator():
452 # ogg files are always from the same project
453 meta = mp3.get_extra_info_json()
454 project = meta.get('project')
457 project = 'CzytamySłuchając'
459 projects.add((project, meta.get('funded_by', '')))
460 total_duration += mp3.duration or 0
464 ogg = ogg_files.get(mp3.name)
467 audiobooks.append(media)
469 projects = sorted(projects)
470 total_duration = '%d:%02d' % (
471 total_duration // 60,
474 return audiobooks, projects, total_duration
476 def wldocument(self, parse_dublincore=True, inherit=True):
477 from catalogue.import_utils import ORMDocProvider
478 from librarian.parser import WLDocument
480 if inherit and self.parent:
481 meta_fallbacks = self.parent.cover_info()
483 meta_fallbacks = None
485 return WLDocument.from_file(
487 provider=ORMDocProvider(self),
488 parse_dublincore=parse_dublincore,
489 meta_fallbacks=meta_fallbacks)
491 def wldocument2(self):
492 from catalogue.import_utils import ORMDocProvider
493 from librarian.document import WLDocument
496 provider=ORMDocProvider(self)
498 doc.meta.update(self.cover_info())
503 def zip_format(format_):
504 def pretty_file_name(book):
505 return "%s/%s.%s" % (
506 book.get_extra_info_json()['author'],
510 field_name = "%s_file" % format_
511 field = getattr(Book, field_name)
512 books = Book.objects.filter(parent=None).exclude(**{field_name: ""}).exclude(preview=True).exclude(findable=False)
513 paths = [(pretty_file_name(b), getattr(b, field_name).path) for b in books.iterator()]
514 return create_zip(paths, field.ZIP)
516 def zip_audiobooks(self, format_):
517 bm = BookMedia.objects.filter(book=self, type=format_)
518 paths = map(lambda bm: (bm.get_nice_filename(), bm.file.path), bm)
521 license = constants.LICENSES.get(
522 m.get_extra_info_json().get('license'), {}
525 licenses.add(license)
526 readme = render_to_string('catalogue/audiobook_zip_readme.txt', {
527 'licenses': licenses,
528 'meta': self.wldocument2().meta,
530 return create_zip(paths, "%s_%s" % (self.slug, format_), {'informacje.txt': readme})
532 def search_index(self, index=None):
533 if not self.findable:
535 from search.index import Index
536 Index.index_book(self)
538 # will make problems in conjunction with paid previews
539 def download_pictures(self, remote_gallery_url):
540 # This is only needed for legacy relative image paths.
541 gallery_path = self.gallery_path()
542 # delete previous files, so we don't include old files in ebooks
543 if os.path.isdir(gallery_path):
544 for filename in os.listdir(gallery_path):
545 file_path = os.path.join(gallery_path, filename)
547 ilustr_elements = list(self.wldocument().edoc.findall('//ilustr'))
549 makedirs(gallery_path)
550 for ilustr in ilustr_elements:
551 ilustr_src = ilustr.get('src')
552 if '/' in ilustr_src:
554 ilustr_path = os.path.join(gallery_path, ilustr_src)
555 urlretrieve('%s/%s' % (remote_gallery_url, ilustr_src), ilustr_path)
557 def load_abstract(self):
558 abstract = self.wldocument(parse_dublincore=False).edoc.getroot().find('.//abstrakt')
559 if abstract is not None:
560 self.abstract = transform_abstrakt(abstract)
567 parser = html.HTMLParser(encoding='utf-8')
568 tree = html.parse(self.html_file.path, parser=parser)
569 toc = tree.find('//div[@id="toc"]/ol')
570 if toc is None or not len(toc):
572 html_link = reverse('book_text', args=[self.slug])
573 for a in toc.findall('.//a'):
574 a.attrib['href'] = html_link + a.attrib['href']
575 self.toc = html.tostring(toc, encoding='unicode')
579 def from_xml_file(cls, xml_file, **kwargs):
580 from django.core.files import File
581 from librarian import dcparser
583 # use librarian to parse meta-data
584 book_info = dcparser.parse(xml_file)
586 if not isinstance(xml_file, File):
587 xml_file = File(open(xml_file))
590 return cls.from_text_and_meta(xml_file, book_info, **kwargs)
595 def from_text_and_meta(cls, raw_file, book_info, overwrite=False, dont_build=None, search_index=True,
596 remote_gallery_url=None, days=0, findable=True):
597 from catalogue import tasks
599 if dont_build is None:
601 dont_build = set.union(set(dont_build), set(app_settings.DONT_BUILD))
603 # check for parts before we do anything
605 if hasattr(book_info, 'parts'):
606 for part_url in book_info.parts:
608 children.append(Book.objects.get(slug=part_url.slug))
609 except Book.DoesNotExist:
610 raise Book.DoesNotExist(_('Book "%s" does not exist.') % part_url.slug)
613 book_slug = book_info.url.slug
614 if re.search(r'[^a-z0-9-]', book_slug):
615 raise ValueError('Invalid characters in slug')
616 book, created = Book.objects.get_or_create(slug=book_slug)
621 book.preview = bool(days)
623 book.preview_until = date.today() + timedelta(days)
626 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
627 # Save shelves for this book
628 book_shelves = list(book.tags.filter(category='set'))
629 old_cover = book.cover_info()
632 book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
634 book.xml_file.set_readable(False)
636 book.findable = findable
637 book.language = book_info.language
638 book.title = book_info.title
639 if book_info.variant_of:
640 book.common_slug = book_info.variant_of.slug
642 book.common_slug = book.slug
643 book.extra_info = json.dumps(book_info.to_dict())
648 meta_tags = Tag.tags_from_info(book_info)
650 for tag in meta_tags:
651 if not tag.for_books:
655 book.tags = set(meta_tags + book_shelves)
656 book.save() # update sort_key_author
658 cover_changed = old_cover != book.cover_info()
659 obsolete_children = set(b for b in book.children.all()
660 if b not in children)
661 notify_cover_changed = []
662 for n, child_book in enumerate(children):
663 new_child = child_book.parent != book
664 child_book.parent = book
665 child_book.parent_number = n
667 if new_child or cover_changed:
668 notify_cover_changed.append(child_book)
669 # Disown unfaithful children and let them cope on their own.
670 for child in obsolete_children:
672 child.parent_number = 0
675 notify_cover_changed.append(child)
677 cls.repopulate_ancestors()
678 tasks.update_counters.delay()
680 if remote_gallery_url:
681 book.download_pictures(remote_gallery_url)
683 # No saves beyond this point.
686 if 'cover' not in dont_build:
687 book.cover.build_delay()
688 book.cover_clean.build_delay()
689 book.cover_thumb.build_delay()
690 book.cover_api_thumb.build_delay()
691 book.simple_cover.build_delay()
692 book.cover_ebookpoint.build_delay()
694 # Build HTML and ebooks.
695 book.html_file.build_delay()
697 for format_ in constants.EBOOK_FORMATS_WITHOUT_CHILDREN:
698 if format_ not in dont_build:
699 getattr(book, '%s_file' % format_).build_delay()
700 for format_ in constants.EBOOK_FORMATS_WITH_CHILDREN:
701 if format_ not in dont_build:
702 getattr(book, '%s_file' % format_).build_delay()
704 if not settings.NO_SEARCH_INDEX and search_index and findable:
705 tasks.index_book.delay(book.id)
707 for child in notify_cover_changed:
708 child.parent_cover_changed()
710 book.update_popularity()
711 tasks.update_references.delay(book.id)
713 cls.published.send(sender=cls, instance=book)
716 def get_master(self):
720 'dramat_wierszowany_l',
721 'dramat_wierszowany_lp',
722 'dramat_wspolczesny', 'liryka_l', 'liryka_lp',
725 from librarian.parser import WLDocument
726 wld = WLDocument.from_file(self.xml_file.path, parse_dublincore=False)
727 root = wld.edoc.getroot()
728 for master in root.iter():
729 if master.tag in master_tags:
732 def update_references(self):
733 from references.models import Entity, Reference
734 master = self.get_master()
738 for i, sec in enumerate(master):
739 for ref in sec.findall('.//ref'):
740 href = ref.attrib.get('href', '')
741 if not href or href in found:
744 entity, created = Entity.objects.get_or_create(
747 ref, created = Reference.objects.get_or_create(
751 ref.first_section = 'sec%d' % (i + 1)
754 Reference.objects.filter(book=self).exclude(entity__uri__in=found).delete()
757 def references(self):
758 return self.reference_set.all().select_related('entity')
762 def repopulate_ancestors(cls):
763 """Fixes the ancestry cache."""
765 cursor = connection.cursor()
766 if connection.vendor == 'postgres':
767 cursor.execute("TRUNCATE catalogue_book_ancestor")
769 WITH RECURSIVE ancestry AS (
770 SELECT book.id, book.parent_id
771 FROM catalogue_book AS book
772 WHERE book.parent_id IS NOT NULL
774 SELECT ancestor.id, book.parent_id
775 FROM ancestry AS ancestor, catalogue_book AS book
776 WHERE ancestor.parent_id = book.id
777 AND book.parent_id IS NOT NULL
779 INSERT INTO catalogue_book_ancestor
780 (from_book_id, to_book_id)
786 cursor.execute("DELETE FROM catalogue_book_ancestor")
787 for b in cls.objects.exclude(parent=None):
789 while parent is not None:
790 b.ancestor.add(parent)
791 parent = parent.parent
796 for anc in self.parent.ancestors:
802 def clear_cache(self):
803 clear_cached_renders(self.mini_box)
804 clear_cached_renders(self.mini_box_nolink)
806 def cover_info(self, inherit=True):
807 """Returns a dictionary to serve as fallback for BookInfo.
809 For now, the only thing inherited is the cover image.
813 for field in ('cover_url', 'cover_by', 'cover_source'):
814 val = self.get_extra_info_json().get(field)
819 if inherit and need and self.parent is not None:
820 parent_info = self.parent.cover_info()
821 parent_info.update(info)
825 def related_themes(self):
826 return Tag.objects.usage_for_queryset(
827 Fragment.objects.filter(models.Q(book=self) | models.Q(book__ancestor=self)),
828 counts=True).filter(category='theme').order_by('-count')
830 def parent_cover_changed(self):
831 """Called when parent book's cover image is changed."""
832 if not self.cover_info(inherit=False):
833 if 'cover' not in app_settings.DONT_BUILD:
834 self.cover.build_delay()
835 self.cover_clean.build_delay()
836 self.cover_thumb.build_delay()
837 self.cover_api_thumb.build_delay()
838 self.simple_cover.build_delay()
839 self.cover_ebookpoint.build_delay()
840 for format_ in constants.EBOOK_FORMATS_WITH_COVERS:
841 if format_ not in app_settings.DONT_BUILD:
842 getattr(self, '%s_file' % format_).build_delay()
843 for child in self.children.all():
844 child.parent_cover_changed()
846 def other_versions(self):
847 """Find other versions (i.e. in other languages) of the book."""
848 return type(self).objects.filter(common_slug=self.common_slug, findable=True).exclude(pk=self.pk)
853 while parent is not None:
854 books.insert(0, parent)
855 parent = parent.parent
858 def pretty_title(self, html_links=False):
859 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
860 books = self.parents() + [self]
861 names.extend([(b.title, b.get_absolute_url()) for b in books])
864 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
866 names = [tag[0] for tag in names]
867 return ', '.join(names)
870 publisher = self.get_extra_info_json()['publisher']
871 if isinstance(publisher, str):
873 elif isinstance(publisher, list):
874 return ', '.join(publisher)
877 def tagged_top_level(cls, tags):
878 """ Returns top-level books tagged with `tags`.
880 It only returns those books which don't have ancestors which are
881 also tagged with those tags.
884 objects = cls.tagged.with_all(tags)
885 return objects.filter(findable=True).exclude(ancestor__in=objects)
888 def book_list(cls, book_filter=None):
889 """Generates a hierarchical listing of all books.
891 Books are optionally filtered with a test function.
896 books = cls.objects.filter(findable=True).order_by('parent_number', 'sort_key').only('title', 'parent', 'slug', 'extra_info')
898 books = books.filter(book_filter).distinct()
900 book_ids = set(b['pk'] for b in books.values("pk").iterator())
901 for book in books.iterator():
902 parent = book.parent_id
903 if parent not in book_ids:
905 books_by_parent.setdefault(parent, []).append(book)
907 for book in books.iterator():
908 books_by_parent.setdefault(book.parent_id, []).append(book)
911 books_by_author = OrderedDict()
912 for tag in Tag.objects.filter(category='author').iterator():
913 books_by_author[tag] = []
915 for book in books_by_parent.get(None, ()):
916 authors = list(book.authors().only('pk'))
918 for author in authors:
919 books_by_author[author].append(book)
923 return books_by_author, orphans, books_by_parent
926 "SP": (1, "szkoła podstawowa"),
927 "SP1": (1, "szkoła podstawowa"),
928 "SP2": (1, "szkoła podstawowa"),
929 "SP3": (1, "szkoła podstawowa"),
930 "P": (1, "szkoła podstawowa"),
931 "G": (2, "gimnazjum"),
936 def audiences_pl(self):
937 audiences = self.get_extra_info_json().get('audiences', [])
938 audiences = sorted(set([self._audiences_pl.get(a, (99, a)) for a in audiences]))
939 return [a[1] for a in audiences]
941 def stage_note(self):
942 stage = self.get_extra_info_json().get('stage')
943 if stage and stage < '0.4':
944 return (_('This work needs modernisation'),
945 reverse('infopage', args=['wymagajace-uwspolczesnienia']))
949 def choose_fragments(self, number):
950 fragments = self.fragments.order_by()
951 fragments_count = fragments.count()
952 if not fragments_count and self.children.exists():
953 fragments = Fragment.objects.filter(book__ancestor=self).order_by()
954 fragments_count = fragments.count()
956 if fragments_count > number:
957 offset = randint(0, fragments_count - number)
960 return fragments[offset : offset + number]
962 return self.parent.choose_fragments(number)
966 def choose_fragment(self):
967 fragments = self.choose_fragments(1)
973 def fragment_data(self):
974 fragment = self.choose_fragment()
977 'title': fragment.book.pretty_title(),
978 'html': re.sub('</?blockquote[^>]*>', '', fragment.get_short_text()),
983 def update_popularity(self):
984 count = self.tags.filter(category='set').values('user').order_by('user').distinct().count()
986 pop = self.popularity
989 except BookPopularity.DoesNotExist:
990 BookPopularity.objects.create(book=self, count=count)
992 def ridero_link(self):
993 return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
995 def like(self, user):
996 from social.utils import likes, get_set, set_sets
997 if not likes(user, self):
998 tag = get_set(user, '')
999 set_sets(user, self, [tag])
1001 def unlike(self, user):
1002 from social.utils import likes, set_sets
1003 if likes(user, self):
1004 set_sets(user, self, [])
1006 def full_sort_key(self):
1007 return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
1009 def cover_color(self):
1010 return WLCover.epoch_colors.get(self.get_extra_info_json().get('epoch'), '#000000')
1012 @cached_render('catalogue/book_mini_box.html')
1018 @cached_render('catalogue/book_mini_box.html')
1019 def mini_box_nolink(self):
1026 class BookPopularity(models.Model):
1027 book = models.OneToOneField(Book, models.CASCADE, related_name='popularity')
1028 count = models.IntegerField(default=0, db_index=True)