Update librarian
[wolnelektury.git] / src / catalogue / models / book.py
index 696b4af..a703c2a 100644 (file)
@@ -1,35 +1,34 @@
-# -*- coding: utf-8 -*-
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from collections import OrderedDict
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from collections import OrderedDict
-from datetime import date
+from datetime import date, timedelta
 from random import randint
 import os.path
 import re
 import urllib
 from django.conf import settings
 from django.db import connection, models, transaction
 from random import randint
 import os.path
 import re
 import urllib
 from django.conf import settings
 from django.db import connection, models, transaction
-from django.db.models import permalink
 import django.dispatch
 from django.contrib.contenttypes.fields import GenericRelation
 import django.dispatch
 from django.contrib.contenttypes.fields import GenericRelation
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _, get_language
 from django.utils.deconstruct import deconstructible
 import jsonfield
 from fnpdjango.storage import BofhFileSystemStorage
 from ssify import flush_ssi_includes
 
 from django.utils.translation import ugettext_lazy as _, get_language
 from django.utils.deconstruct import deconstructible
 import jsonfield
 from fnpdjango.storage import BofhFileSystemStorage
 from ssify import flush_ssi_includes
 
+from librarian.cover import WLCover
 from librarian.html import transform_abstrakt
 from newtagging import managers
 from catalogue import constants
 from catalogue.fields import EbookField
 from catalogue.models import Tag, Fragment, BookMedia
 from librarian.html import transform_abstrakt
 from newtagging import managers
 from catalogue import constants
 from catalogue.fields import EbookField
 from catalogue.models import Tag, Fragment, BookMedia
-from catalogue.utils import create_zip, gallery_url, gallery_path, split_tags
+from catalogue.utils import create_zip, gallery_url, gallery_path, split_tags, get_random_hash
 from catalogue.models.tag import prefetched_relations
 from catalogue import app_settings
 from catalogue import tasks
 from catalogue.models.tag import prefetched_relations
 from catalogue import app_settings
 from catalogue import tasks
-from wolnelektury.utils import makedirs
+from wolnelektury.utils import makedirs, cached_render, clear_cached_renders
 
 bofh_storage = BofhFileSystemStorage()
 
 
 bofh_storage = BofhFileSystemStorage()
 
@@ -72,8 +71,10 @@ class Book(models.Model):
     wiki_link = models.CharField(blank=True, max_length=240)
     print_on_demand = models.BooleanField(_('print on demand'), default=False)
     recommended = models.BooleanField(_('recommended'), default=False)
     wiki_link = models.CharField(blank=True, max_length=240)
     print_on_demand = models.BooleanField(_('print on demand'), default=False)
     recommended = models.BooleanField(_('recommended'), default=False)
+    audio_length = models.CharField(_('audio length'), blank=True, max_length=8)
     preview = models.BooleanField(_('preview'), default=False)
     preview_until = models.DateField(_('preview until'), blank=True, null=True)
     preview = models.BooleanField(_('preview'), default=False)
     preview_until = models.DateField(_('preview until'), blank=True, null=True)
+    preview_key = models.CharField(max_length=32, blank=True, null=True)
 
     # files generated during publication
     cover = EbookField(
 
     # files generated during publication
     cover = EbookField(
@@ -100,7 +101,7 @@ class Book(models.Model):
     ebook_formats = constants.EBOOK_FORMATS
     formats = ebook_formats + ['html', 'xml']
 
     ebook_formats = constants.EBOOK_FORMATS
     formats = ebook_formats + ['html', 'xml']
 
-    parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
+    parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True, related_name='children')
     ancestor = models.ManyToManyField('self', blank=True, editable=False, related_name='descendant', symmetrical=False)
 
     cached_author = models.CharField(blank=True, max_length=240, db_index=True)
     ancestor = models.ManyToManyField('self', blank=True, editable=False, related_name='descendant', symmetrical=False)
 
     cached_author = models.CharField(blank=True, max_length=240, db_index=True)
@@ -114,7 +115,7 @@ class Book(models.Model):
     html_built = django.dispatch.Signal()
     published = django.dispatch.Signal()
 
     html_built = django.dispatch.Signal()
     published = django.dispatch.Signal()
 
-    short_html_url_name = 'catalogue_book_short'
+    SORT_KEY_SEP = '$'
 
     class AlreadyExists(Exception):
         pass
 
     class AlreadyExists(Exception):
         pass
@@ -125,7 +126,7 @@ class Book(models.Model):
         verbose_name_plural = _('books')
         app_label = 'catalogue'
 
         verbose_name_plural = _('books')
         app_label = 'catalogue'
 
-    def __unicode__(self):
+    def __str__(self):
         return self.title
 
     def get_initial(self):
         return self.title
 
     def get_initial(self):
@@ -137,6 +138,15 @@ class Book(models.Model):
     def authors(self):
         return self.tags.filter(category='author')
 
     def authors(self):
         return self.tags.filter(category='author')
 
+    def epochs(self):
+        return self.tags.filter(category='epoch')
+
+    def genres(self):
+        return self.tags.filter(category='genre')
+
+    def kinds(self):
+        return self.tags.filter(category='kind')
+
     def tag_unicode(self, category):
         relations = prefetched_relations(self, category)
         if relations:
     def tag_unicode(self, category):
         relations = prefetched_relations(self, category)
         if relations:
@@ -150,6 +160,15 @@ class Book(models.Model):
     def author_unicode(self):
         return self.cached_author
 
     def author_unicode(self):
         return self.cached_author
 
+    def kind_unicode(self):
+        return self.tag_unicode('kind')
+
+    def epoch_unicode(self):
+        return self.tag_unicode('epoch')
+
+    def genre_unicode(self):
+        return self.tag_unicode('genre')
+
     def translator(self):
         translators = self.extra_info.get('translators')
         if not translators:
     def translator(self):
         translators = self.extra_info.get('translators')
         if not translators:
@@ -168,7 +187,7 @@ class Book(models.Model):
         from sortify import sortify
 
         self.sort_key = sortify(self.title)[:120]
         from sortify import sortify
 
         self.sort_key = sortify(self.title)[:120]
-        self.title = unicode(self.title)  # ???
+        self.title = str(self.title)  # ???
 
         try:
             author = self.authors().first().sort_key
 
         try:
             author = self.authors().first().sort_key
@@ -179,18 +198,15 @@ class Book(models.Model):
         self.cached_author = self.tag_unicode('author')
         self.has_audience = 'audience' in self.extra_info
 
         self.cached_author = self.tag_unicode('author')
         self.has_audience = 'audience' in self.extra_info
 
+        if self.preview and not self.preview_key:
+            self.preview_key = get_random_hash(self.slug)[:32]
+
         ret = super(Book, self).save(force_insert, force_update, **kwargs)
 
         return ret
 
         ret = super(Book, self).save(force_insert, force_update, **kwargs)
 
         return ret
 
-    @permalink
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return 'catalogue.views.book_detail', [self.slug]
-
-    @staticmethod
-    @permalink
-    def create_url(slug):
-        return 'catalogue.views.book_detail', [slug]
+        return reverse('book_detail', args=[self.slug])
 
     def gallery_path(self):
         return gallery_path(self.slug)
 
     def gallery_path(self):
         return gallery_path(self.slug)
@@ -211,6 +227,30 @@ class Book(models.Model):
     def is_foreign(self):
         return self.language_code() != settings.LANGUAGE_CODE
 
     def is_foreign(self):
         return self.language_code() != settings.LANGUAGE_CODE
 
+    def set_audio_length(self):
+        length = self.get_audio_length()
+        if length > 0:
+            self.audio_length = self.format_audio_length(length)
+            self.save()
+
+    @staticmethod
+    def format_audio_length(seconds):
+        if seconds < 60*60:
+            minutes = seconds // 60
+            seconds = seconds % 60
+            return '%d:%02d' % (minutes, seconds)
+        else:
+            hours = seconds // 3600
+            minutes = seconds % 3600 // 60
+            seconds = seconds % 60
+            return '%d:%02d:%02d' % (hours, minutes, seconds)
+
+    def get_audio_length(self):
+        total = 0
+        for media in self.get_mp3() or ():
+            total += app_settings.GET_MP3_LENGTH(media.file.path)
+        return int(total)
+
     def has_media(self, type_):
         if type_ in Book.formats:
             return bool(getattr(self, "%s_file" % type_))
     def has_media(self, type_):
         if type_ in Book.formats:
             return bool(getattr(self, "%s_file" % type_))
@@ -245,7 +285,7 @@ class Book(models.Model):
         media = self.get_media(format_)
         if media:
             if self.preview:
         media = self.get_media(format_)
         if media:
             if self.preview:
-                return reverse('embargo_link', kwargs={'slug': self.slug, 'format_': format_})
+                return reverse('embargo_link', kwargs={'key': self.preview_key, 'slug': self.slug, 'format_': format_})
             else:
                 return media.url
         else:
             else:
                 return media.url
         else:
@@ -277,19 +317,18 @@ class Book(models.Model):
     has_description.short_description = _('description')
     has_description.boolean = True
 
     has_description.short_description = _('description')
     has_description.boolean = True
 
-    # ugly ugly ugly
     def has_mp3_file(self):
     def has_mp3_file(self):
-        return bool(self.has_media("mp3"))
+        return self.has_media("mp3")
     has_mp3_file.short_description = 'MP3'
     has_mp3_file.boolean = True
 
     def has_ogg_file(self):
     has_mp3_file.short_description = 'MP3'
     has_mp3_file.boolean = True
 
     def has_ogg_file(self):
-        return bool(self.has_media("ogg"))
+        return self.has_media("ogg")
     has_ogg_file.short_description = 'OGG'
     has_ogg_file.boolean = True
 
     def has_daisy_file(self):
     has_ogg_file.short_description = 'OGG'
     has_ogg_file.boolean = True
 
     def has_daisy_file(self):
-        return bool(self.has_media("daisy"))
+        return self.has_media("daisy")
     has_daisy_file.short_description = 'DAISY'
     has_daisy_file.boolean = True
 
     has_daisy_file.short_description = 'DAISY'
     has_daisy_file.boolean = True
 
@@ -363,7 +402,7 @@ class Book(models.Model):
                 index.index_tags()
             if commit:
                 index.index.commit()
                 index.index_tags()
             if commit:
                 index.index.commit()
-        except Exception, e:
+        except Exception as e:
             index.index.rollback()
             raise e
 
             index.index.rollback()
             raise e
 
@@ -433,7 +472,7 @@ class Book(models.Model):
             old_cover = None
             book.preview = bool(days)
             if book.preview:
             old_cover = None
             book.preview = bool(days)
             if book.preview:
-                book.preview_until = date.today()
+                book.preview_until = date.today() + timedelta(days)
         else:
             if not overwrite:
                 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
         else:
             if not overwrite:
                 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
@@ -554,6 +593,8 @@ class Book(models.Model):
                     parent = parent.parent
 
     def flush_includes(self, languages=True):
                     parent = parent.parent
 
     def flush_includes(self, languages=True):
+        clear_cached_renders(self.mini_box)
+        clear_cached_renders(self.mini_box_nolink)
         if not languages:
             return
         if languages is True:
         if not languages:
             return
         if languages is True:
@@ -561,8 +602,6 @@ class Book(models.Model):
         flush_ssi_includes([
             template % (self.pk, lang)
             for template in [
         flush_ssi_includes([
             template % (self.pk, lang)
             for template in [
-                '/katalog/b/%d/mini.%s.html',
-                '/katalog/b/%d/mini_nolink.%s.html',
                 '/katalog/b/%d/short.%s.html',
                 '/katalog/b/%d/wide.%s.html',
                 '/api/include/book/%d.%s.json',
                 '/katalog/b/%d/short.%s.html',
                 '/katalog/b/%d/wide.%s.html',
                 '/api/include/book/%d.%s.json',
@@ -634,7 +673,7 @@ class Book(models.Model):
 
     def publisher(self):
         publisher = self.extra_info['publisher']
 
     def publisher(self):
         publisher = self.extra_info['publisher']
-        if isinstance(publisher, basestring):
+        if isinstance(publisher, str):
             return publisher
         elif isinstance(publisher, list):
             return ', '.join(publisher)
             return publisher
         elif isinstance(publisher, list):
             return ', '.join(publisher)
@@ -659,7 +698,7 @@ class Book(models.Model):
         """
 
         books_by_parent = {}
         """
 
         books_by_parent = {}
-        books = cls.objects.order_by('parent_number', 'sort_key').only('title', 'parent', 'slug')
+        books = cls.objects.order_by('parent_number', 'sort_key').only('title', 'parent', 'slug', 'extra_info')
         if book_filter:
             books = books.filter(book_filter).distinct()
 
         if book_filter:
             books = books.filter(book_filter).distinct()
 
@@ -728,7 +767,10 @@ class Book(models.Model):
     def fragment_data(self):
         fragment = self.choose_fragment()
         if fragment:
     def fragment_data(self):
         fragment = self.choose_fragment()
         if fragment:
-            return {'title': fragment.book.pretty_title(), 'html': fragment.get_short_text()}
+            return {
+                'title': fragment.book.pretty_title(),
+                'html': re.sub('</?blockquote[^>]*>', '', fragment.get_short_text()),
+            }
         else:
             return None
 
         else:
             return None
 
@@ -744,6 +786,35 @@ class Book(models.Model):
     def ridero_link(self):
         return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
 
     def ridero_link(self):
         return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
 
+    def like(self, user):
+        from social.utils import likes, get_set, set_sets
+        if not likes(user, self):
+            tag = get_set(user, '')
+            set_sets(user, self, [tag])
+
+    def unlike(self, user):
+        from social.utils import likes, set_sets
+        if likes(user, self):
+            set_sets(user, self, [])
+
+    def full_sort_key(self):
+        return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
+
+    def cover_color(self):
+        return WLCover.epoch_colors.get(self.extra_info.get('epoch'), '#000000')
+
+    @cached_render('catalogue/book_mini_box.html')
+    def mini_box(self):
+        return {
+            'book': self
+        }
+
+    @cached_render('catalogue/book_mini_box.html')
+    def mini_box_nolink(self):
+        return {
+            'book': self,
+            'no_link': True,
+        }
 
 def add_file_fields():
     for format_ in Book.formats:
 
 def add_file_fields():
     for format_ in Book.formats:
@@ -762,9 +833,10 @@ def add_file_fields():
             default=''
         ).contribute_to_class(Book, field_name)
 
             default=''
         ).contribute_to_class(Book, field_name)
 
+
 add_file_fields()
 
 
 class BookPopularity(models.Model):
 add_file_fields()
 
 
 class BookPopularity(models.Model):
-    book = models.OneToOneField(Book, related_name='popularity')
+    book = models.OneToOneField(Book, models.CASCADE, related_name='popularity')
     count = models.IntegerField(default=0, db_index=True)
     count = models.IntegerField(default=0, db_index=True)