separate Book.build_html method
[wolnelektury.git] / apps / catalogue / models.py
index 84bcfd3..568679b 100644 (file)
@@ -2,6 +2,8 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
+from datetime import datetime
+
 from django.db import models
 from django.db.models import permalink, Q
 from django.utils.translation import ugettext_lazy as _
 from django.db import models
 from django.db.models import permalink, Q
 from django.utils.translation import ugettext_lazy as _
@@ -17,12 +19,14 @@ from django.conf import settings
 
 from newtagging.models import TagBase, tags_updated
 from newtagging import managers
 
 from newtagging.models import TagBase, tags_updated
 from newtagging import managers
-from catalogue.fields import JSONField
+from catalogue.fields import JSONField, OverwritingFileField
+from catalogue.utils import ExistingFile
 
 from librarian import dcparser, html, epub, NoDublinCore
 import mutagen
 from mutagen import id3
 from slughifi import slughifi
 
 from librarian import dcparser, html, epub, NoDublinCore
 import mutagen
 from mutagen import id3
 from slughifi import slughifi
+from sortify import sortify
 
 
 TAG_CATEGORIES = (
 
 
 TAG_CATEGORIES = (
@@ -65,6 +69,9 @@ class Tag(TagBase):
     gazeta_link = models.CharField(blank=True, max_length=240)
     wiki_link = models.CharField(blank=True, max_length=240)
 
     gazeta_link = models.CharField(blank=True, max_length=240)
     wiki_link = models.CharField(blank=True, max_length=240)
 
+    created_at    = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
+    changed_at    = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
+
     class UrlDeprecationWarning(DeprecationWarning):
         pass
 
     class UrlDeprecationWarning(DeprecationWarning):
         pass
 
@@ -176,14 +183,14 @@ def book_upload_path(ext=None, maxlen=100):
             name = slughifi(filename.split(".")[0])
         else:
             name = slughifi(media.name)
             name = slughifi(filename.split(".")[0])
         else:
             name = slughifi(media.name)
-        return 'lektura/%s.%s' % (name[:maxlen-len('lektura/.%s' % ext)-4], ext)
+        return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext)
     return get_dynamic_path
 
 
 class BookMedia(models.Model):
     type        = models.CharField(_('type'), choices=MEDIA_FORMATS, max_length="100")
     name        = models.CharField(_('name'), max_length="100")
     return get_dynamic_path
 
 
 class BookMedia(models.Model):
     type        = models.CharField(_('type'), choices=MEDIA_FORMATS, max_length="100")
     name        = models.CharField(_('name'), max_length="100")
-    file        = models.FileField(_('file'), upload_to=book_upload_path())
+    file        = OverwritingFileField(_('file'), upload_to=book_upload_path())
     uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False)
     extra_info  = JSONField(_('extra information'), default='{}', editable=False)
     book = models.ForeignKey('Book', related_name='media')
     uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False)
     extra_info  = JSONField(_('extra information'), default='{}', editable=False)
     book = models.ForeignKey('Book', related_name='media')
@@ -198,6 +205,15 @@ class BookMedia(models.Model):
         verbose_name_plural = _('book media')
 
     def save(self, *args, **kwargs):
         verbose_name_plural = _('book media')
 
     def save(self, *args, **kwargs):
+        try:
+            old = BookMedia.objects.get(pk=self.pk)
+        except BookMedia.DoesNotExist, e:
+            pass
+        else:
+            # if name changed, change the file name, too
+            if slughifi(self.name) != slughifi(old.name):
+                self.file.save(None, ExistingFile(self.file.path), save=False, leave=True)
+
         super(BookMedia, self).save(*args, **kwargs)
         extra_info = self.get_extra_info_value()
         extra_info.update(self.read_meta())
         super(BookMedia, self).save(*args, **kwargs)
         extra_info = self.get_extra_info_value()
         extra_info.update(self.read_meta())
@@ -210,12 +226,16 @@ class BookMedia(models.Model):
             Reads some metadata from the audiobook.
         """
 
             Reads some metadata from the audiobook.
         """
 
-        artist_name = director_name = ''
+        artist_name = director_name = project = funded_by = ''
         if self.type == 'mp3':
             try:
                 audio = id3.ID3(self.file.path)
                 artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
                 director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
         if self.type == 'mp3':
             try:
                 audio = id3.ID3(self.file.path)
                 artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
                 director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
+                project = ", ".join([t.data for t in audio.getall('PRIV') 
+                        if t.owner=='wolnelektury.pl?project'])
+                funded_by = ", ".join([t.data for t in audio.getall('PRIV') 
+                        if t.owner=='wolnelektury.pl?funded_by'])
             except:
                 pass
         elif self.type == 'ogg':
             except:
                 pass
         elif self.type == 'ogg':
@@ -223,11 +243,14 @@ class BookMedia(models.Model):
                 audio = mutagen.File(self.file.path)
                 artist_name = ', '.join(audio.get('artist', []))
                 director_name = ', '.join(audio.get('conductor', []))
                 audio = mutagen.File(self.file.path)
                 artist_name = ', '.join(audio.get('artist', []))
                 director_name = ', '.join(audio.get('conductor', []))
+                project = ", ".join(audio.get('project', []))
+                funded_by = ", ".join(audio.get('funded_by', []))
             except:
                 pass
         else:
             return {}
             except:
                 pass
         else:
             return {}
-        return {'artist_name': artist_name, 'director_name': director_name}
+        return {'artist_name': artist_name, 'director_name': director_name,
+                'project': project, 'funded_by': funded_by}
 
     @staticmethod
     def read_source_sha1(filepath, filetype):
 
     @staticmethod
     def read_source_sha1(filepath, filetype):
@@ -239,7 +262,7 @@ class BookMedia(models.Model):
             try:
                 audio = id3.ID3(filepath)
                 return [t.data for t in audio.getall('PRIV') 
             try:
                 audio = id3.ID3(filepath)
                 return [t.data for t in audio.getall('PRIV') 
-                        if t.owner=='http://wolnelektury.pl?flac_sha1'][0]
+                        if t.owner=='wolnelektury.pl?flac_sha1'][0]
             except:
                 return None
         elif filetype == 'ogg':
             except:
                 return None
         elif filetype == 'ogg':
@@ -254,12 +277,14 @@ class BookMedia(models.Model):
 
 class Book(models.Model):
     title         = models.CharField(_('title'), max_length=120)
 
 class Book(models.Model):
     title         = models.CharField(_('title'), max_length=120)
+    sort_key = models.CharField(_('sort_key'), max_length=120, db_index=True, editable=False)
     slug          = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
     description   = models.TextField(_('description'), blank=True)
     slug          = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
     description   = models.TextField(_('description'), blank=True)
-    created_at    = models.DateTimeField(_('creation date'), auto_now_add=True)
+    created_at    = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
+    changed_at    = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
     _short_html   = models.TextField(_('short HTML'), editable=False)
     parent_number = models.IntegerField(_('parent number'), default=0)
     _short_html   = models.TextField(_('short HTML'), editable=False)
     parent_number = models.IntegerField(_('parent number'), default=0)
-    extra_info    = JSONField(_('extra information'))
+    extra_info    = JSONField(_('extra information'), default='{}')
     gazeta_link   = models.CharField(blank=True, max_length=240)
     wiki_link     = models.CharField(blank=True, max_length=240)
     # files generated during publication
     gazeta_link   = models.CharField(blank=True, max_length=240)
     wiki_link     = models.CharField(blank=True, max_length=240)
     # files generated during publication
@@ -281,7 +306,7 @@ class Book(models.Model):
         pass
 
     class Meta:
         pass
 
     class Meta:
-        ordering = ('title',)
+        ordering = ('sort_key',)
         verbose_name = _('book')
         verbose_name_plural = _('books')
 
         verbose_name = _('book')
         verbose_name_plural = _('books')
 
@@ -289,6 +314,8 @@ class Book(models.Model):
         return self.title
 
     def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
         return self.title
 
     def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
+        self.sort_key = sortify(self.title)
+
         if reset_short_html:
             # Reset _short_html during save
             update = {}
         if reset_short_html:
             # Reset _short_html during save
             update = {}
@@ -520,7 +547,61 @@ class Book(models.Model):
         out = StringIO()
         text.transform(open(self.xml_file.path), out)
         self.txt_file.save('%s.txt' % self.slug, ContentFile(out.getvalue()))
         out = StringIO()
         text.transform(open(self.xml_file.path), out)
         self.txt_file.save('%s.txt' % self.slug, ContentFile(out.getvalue()))
-        self.save()
+
+
+    def build_html(self):
+        from tempfile import NamedTemporaryFile
+        from markupstring import MarkupString
+
+        meta_tags = list(self.tags.filter(
+            category__in=('author', 'epoch', 'genre', 'kind')))
+        book_tag = self.book_tag()
+
+        html_file = NamedTemporaryFile()
+        if html.transform(self.xml_file.path, html_file, parse_dublincore=False):
+            self.html_file.save('%s.html' % self.slug, File(html_file))
+
+            # get ancestor l-tags for adding to new fragments
+            ancestor_tags = []
+            p = self.parent
+            while p:
+                ancestor_tags.append(p.book_tag())
+                p = p.parent
+
+            # Delete old fragments and create them from scratch
+            self.fragments.all().delete()
+            # Extract fragments
+            closed_fragments, open_fragments = html.extract_fragments(self.html_file.path)
+            for fragment in closed_fragments.values():
+                try:
+                    theme_names = [s.strip() for s in fragment.themes.split(',')]
+                except AttributeError:
+                    continue
+                themes = []
+                for theme_name in theme_names:
+                    if not theme_name:
+                        continue
+                    tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name), category='theme')
+                    if created:
+                        tag.name = theme_name
+                        tag.sort_key = theme_name.lower()
+                        tag.save()
+                    themes.append(tag)
+                if not themes:
+                    continue
+
+                text = fragment.to_string()
+                short_text = ''
+                if (len(MarkupString(text)) > 240):
+                    short_text = unicode(MarkupString(text)[:160])
+                new_fragment = Fragment.objects.create(anchor=fragment.id, book=self,
+                    text=text, short_text=short_text)
+
+                new_fragment.save()
+                new_fragment.tags = set(meta_tags + themes + [book_tag] + ancestor_tags)
+            self.save()
+            return True
+        return False
 
 
     @classmethod
 
 
     @classmethod
@@ -539,9 +620,6 @@ class Book(models.Model):
     @classmethod
     def from_text_and_meta(cls, raw_file, book_info, overwrite=False, build_epub=True, build_txt=True):
         import re
     @classmethod
     def from_text_and_meta(cls, raw_file, book_info, overwrite=False, build_epub=True, build_txt=True):
         import re
-        from tempfile import NamedTemporaryFile
-        from markupstring import MarkupString
-        from django.core.files.storage import default_storage
 
         # check for parts before we do anything
         children = []
 
         # check for parts before we do anything
         children = []
@@ -573,7 +651,7 @@ class Book(models.Model):
         book._short_html = ''
         book.save()
 
         book._short_html = ''
         book.save()
 
-        book_tags = []
+        meta_tags = []
         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
         for field_name, category in categories:
             try:
         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
         for field_name, category in categories:
             try:
@@ -588,11 +666,11 @@ class Book(models.Model):
                 tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category)
                 if created:
                     tag.name = tag_name
                 tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category)
                 if created:
                     tag.name = tag_name
-                    tag.sort_key = tag_sort_key.lower()
+                    tag.sort_key = sortify(tag_sort_key.lower())
                     tag.save()
                     tag.save()
-                book_tags.append(tag)
+                meta_tags.append(tag)
 
 
-        book.tags = set(book_tags + book_shelves)
+        book.tags = set(meta_tags + book_shelves)
 
         book_tag = book.book_tag()
 
 
         book_tag = book.book_tag()
 
@@ -607,47 +685,7 @@ class Book(models.Model):
         # delete old fragments when overwriting
         book.fragments.all().delete()
 
         # delete old fragments when overwriting
         book.fragments.all().delete()
 
-        html_file = NamedTemporaryFile()
-        if html.transform(book.xml_file.path, html_file, parse_dublincore=False):
-            book.html_file.save('%s.html' % book.slug, File(html_file), save=False)
-
-            # get ancestor l-tags for adding to new fragments
-            ancestor_tags = []
-            p = book.parent
-            while p:
-                ancestor_tags.append(p.book_tag())
-                p = p.parent
-
-            # Extract fragments
-            closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
-            for fragment in closed_fragments.values():
-                try:
-                    theme_names = [s.strip() for s in fragment.themes.split(',')]
-                except AttributeError:
-                    continue
-                themes = []
-                for theme_name in theme_names:
-                    if not theme_name:
-                        continue
-                    tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name), category='theme')
-                    if created:
-                        tag.name = theme_name
-                        tag.sort_key = theme_name.lower()
-                        tag.save()
-                    themes.append(tag)
-                if not themes:
-                    continue
-
-                text = fragment.to_string()
-                short_text = ''
-                if (len(MarkupString(text)) > 240):
-                    short_text = unicode(MarkupString(text)[:160])
-                new_fragment, created = Fragment.objects.get_or_create(anchor=fragment.id, book=book,
-                    defaults={'text': text, 'short_text': short_text})
-
-                new_fragment.save()
-                new_fragment.tags = set(book_tags + themes + [book_tag] + ancestor_tags)
-
+        if book.build_html():
             if not settings.NO_BUILD_TXT and build_txt:
                 book.build_txt()
 
             if not settings.NO_BUILD_TXT and build_txt:
                 book.build_txt()
 
@@ -734,6 +772,24 @@ class Book(models.Model):
 
         return ', '.join(names)
 
 
         return ', '.join(names)
 
+    @classmethod
+    def tagged_top_level(cls, tags):
+        """ Returns top-level books tagged with `tags'.
+
+        It only returns those books which don't have ancestors which are
+        also tagged with those tags.
+
+        """
+        # get relevant books and their tags
+        objects = cls.tagged.with_all(tags)
+        # eliminate descendants
+        l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in objects])
+        descendants_keys = [book.pk for book in cls.tagged.with_any(l_tags)]
+        if descendants_keys:
+            objects = objects.exclude(pk__in=descendants_keys)
+
+        return objects
+
 
 class Fragment(models.Model):
     text = models.TextField()
 
 class Fragment(models.Model):
     text = models.TextField()
@@ -789,7 +845,8 @@ class FileRecord(models.Model):
 
 def _tags_updated_handler(sender, affected_tags, **kwargs):
     # reset tag global counter
 
 def _tags_updated_handler(sender, affected_tags, **kwargs):
     # reset tag global counter
-    Tag.objects.filter(pk__in=[tag.pk for tag in affected_tags]).update(book_count=None)
+    # we want Tag.changed_at updated for API to know the tag was touched
+    Tag.objects.filter(pk__in=[tag.pk for tag in affected_tags]).update(book_count=None, changed_at=datetime.now())
 
     # if book tags changed, reset book tag counter
     if isinstance(sender, Book) and \
 
     # if book tags changed, reset book tag counter
     if isinstance(sender, Book) and \