- inheriting covers after parents,
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 21 Aug 2012 12:35:42 +0000 (14:35 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 21 Aug 2012 12:35:42 +0000 (14:35 +0200)
- DRY: move build methods to relevant fields, simplify adding formats (in catalogue.constants)
- add CATALOGUE_{DONT_BUILD,FORMAT_ZIPS} instead of many NO_BUILD_* and ALL_*_ZIP settings, move to catalogue.app_settings
- catalogue.management.importbooks: one option for skipping formats
- use override_settings in tests
- minor test fixes

15 files changed:
apps/catalogue/__init__.py
apps/catalogue/constants.py
apps/catalogue/fields.py
apps/catalogue/management/commands/importbooks.py
apps/catalogue/migrations/0005_auto__chg_field_book_pdf_file__chg_field_book_html_file__chg_field_boo.py [new file with mode: 0644]
apps/catalogue/models/book.py
apps/catalogue/tasks.py
apps/catalogue/test_utils.py
apps/catalogue/tests/book_import.py
apps/catalogue/tests/bookmedia.py
apps/catalogue/utils.py
apps/picture/tests/picture_import.py
apps/search/tests/index.py
lib/librarian
wolnelektury/settings/custom.py

index e69de29..2d0d89d 100644 (file)
@@ -0,0 +1,45 @@
+# -*- 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.
+#
+import logging
+from django.conf import settings as settings
+from catalogue.utils import AppSettings
+
+
+class Settings(AppSettings):
+    """Default settings for catalogue app."""
+    DEFAULT_LANGUAGE = u'pol'
+    # PDF needs TeXML + XeLaTeX, MOBI needs Calibre.
+    DONT_BUILD = {'pdf', 'mobi'}
+    FORMAT_ZIPS = {
+            'epub': 'wolnelektury_pl_epub',
+            'pdf': 'wolnelektury_pl_pdf',
+            'mobi': 'wolnelektury_pl_mobi',
+            'fb2': 'wolnelektury_pl_fb2',
+        }
+
+    def _more_DONT_BUILD(self, value):
+        for format_ in ['cover', 'pdf', 'epub', 'mobi', 'fb2', 'txt']:
+            attname = 'NO_BUILD_%s' % format_.upper()
+            if hasattr(settings, attname):
+                logging.warn("%s is deprecated, "
+                        "use CATALOGUE_DONT_BUILD instead", attname)
+                if getattr(settings, attname):
+                    value.add(format_)
+                else:
+                    value.remove(format_)
+        return value
+
+    def _more_FORMAT_ZIPS(self, value):
+        for format_ in ['epub', 'pdf', 'mobi', 'fb2']:
+            attname = 'ALL_%s_ZIP' % format_.upper()
+            if hasattr(settings, attname):
+                logging.warn("%s is deprecated, "
+                        "use CATALOGUE_FORMAT_ZIPS[%s] instead", 
+                            attname, format_)
+                value[format_] = getattr(settings, attname)
+        return value
+
+
+app_settings = Settings('CATALOGUE')
index e1c92f8..1ad0b1b 100644 (file)
@@ -10,3 +10,12 @@ LICENSES = {
         'description': _('Creative Commons Attribution-ShareAlike 3.0 Unported'),
     },
 }
         'description': _('Creative Commons Attribution-ShareAlike 3.0 Unported'),
     },
 }
+
+# Those will be generated only for books with own HTML.
+EBOOK_FORMATS_WITHOUT_CHILDREN = ['txt', 'fb2']
+# Those will be generated for all books.
+EBOOK_FORMATS_WITH_CHILDREN = ['pdf', 'epub', 'mobi']
+# Those will be generated when inherited cover changes.
+EBOOK_FORMATS_WITH_COVERS = ['mobi']
+
+EBOOK_FORMATS = EBOOK_FORMATS_WITHOUT_CHILDREN + EBOOK_FORMATS_WITH_CHILDREN
index 5ab78eb..2c4e4d5 100644 (file)
@@ -2,8 +2,182 @@
 # 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 django.conf import settings
+from django.core.files import File
 from django.db import models
 from django.db.models.fields.files import FieldFile
 from django.db import models
 from django.db.models.fields.files import FieldFile
+from catalogue import app_settings
+from catalogue.utils import remove_zip, truncate_html_words
+from celery import Task
+from celery.task import task
+from waiter.utils import clear_cache
+
+
+class EbookFieldFile(FieldFile):
+    """Represents contents of an ebook file field."""
+
+    def build(self):
+        """Build the ebook immediately."""
+        return self.field.builder(self)
+
+    def build_delay(self):
+        """Builds the ebook in a delayed task."""
+        return self.field.builder.delay(self)
+
+
+class EbookField(models.FileField):
+    """Represents an ebook file field, attachable to a model."""
+    attr_class = EbookFieldFile
+
+    def __init__(self, format_name, *args, **kwargs):
+        super(EbookField, self).__init__(*args, **kwargs)
+        self.format_name = format_name
+
+    @property
+    def builder(self):
+        """Finds a celery task suitable for the format of the field."""
+        return BuildEbook.for_format(self.format_name)
+
+    def contribute_to_class(self, cls, name):
+        super(EbookField, self).contribute_to_class(cls, name)
+
+        def has(model_instance):
+            return bool(getattr(model_instance, self.attname, None))
+        has.__doc__ = None
+        has.__name__ = "has_%s" % self.attname
+        has.short_description = self.name
+        has.boolean = True
+        setattr(cls, 'has_%s' % self.attname, has)
+
+
+class BuildEbook(Task):
+    formats = {}
+
+    @classmethod
+    def register(cls, format_name):
+        """A decorator for registering subclasses for particular formats."""
+        def wrapper(builder):
+            cls.formats[format_name] = builder
+            return builder
+        return wrapper
+
+    @classmethod
+    def for_format(cls, format_name):
+        """Returns a celery task suitable for specified format."""
+        return cls.formats.get(format_name, BuildEbookTask)
+
+    @staticmethod
+    def transform(wldoc, fieldfile):
+        """Transforms an librarian.WLDocument into an librarian.OutputFile.
+
+        By default, it just calls relevant wldoc.as_??? method.
+
+        """
+        return getattr(wldoc, "as_%s" % fieldfile.field.format_name)()
+
+    def run(self, fieldfile):
+        book = fieldfile.instance
+        out = self.transform(book.wldocument(), fieldfile)
+        fieldfile.save(None, File(open(out.get_filename())), save=False)
+        if book.pk is not None:
+            type(book).objects.filter(pk=book.pk).update(**{
+                fieldfile.field.attname: fieldfile
+            })
+        if fieldfile.field.format_name in app_settings.FORMAT_ZIPS:
+            remove_zip(app_settings.FORMAT_ZIPS[fieldfile.field.format_name])
+# Don't decorate BuildEbook, because we want to subclass it.
+BuildEbookTask = task(BuildEbook, ignore_result=True)
+
+
+@BuildEbook.register('txt')
+@task(ignore_result=True)
+class BuildTxt(BuildEbook):
+    @staticmethod
+    def transform(wldoc, fieldfile):
+        return wldoc.as_text()
+
+
+@BuildEbook.register('pdf')
+@task(ignore_result=True)
+class BuildPdf(BuildEbook):
+    @staticmethod
+    def transform(wldoc, fieldfile):
+        return wldoc.as_pdf(morefloats=settings.LIBRARIAN_PDF_MOREFLOATS)
+
+    def run(self, fieldfile):
+        BuildEbook.run(self, fieldfile)
+        clear_cache(fieldfile.instance.slug)
+
+
+@BuildEbook.register('html')
+@task(ignore_result=True)
+class BuildHtml(BuildEbook):
+    def run(self, fieldfile):
+        from django.core.files.base import ContentFile
+        from slughifi import slughifi
+        from sortify import sortify
+        from librarian import html
+        from catalogue.models import Fragment, Tag
+
+        book = fieldfile.instance
+
+        meta_tags = list(book.tags.filter(
+            category__in=('author', 'epoch', 'genre', 'kind')))
+        book_tag = book.book_tag()
+
+        html_output = self.transform(
+                        book.wldocument(parse_dublincore=False),
+                        fieldfile)
+        if html_output:
+            fieldfile.save(None, ContentFile(html_output.get_string()),
+                    save=False)
+            type(book).objects.filter(pk=book.pk).update(**{
+                fieldfile.field.attname: fieldfile
+            })
+
+            # 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
+
+            # Delete old fragments and create them from scratch
+            book.fragments.all().delete()
+            # Extract fragments
+            closed_fragments, open_fragments = html.extract_fragments(fieldfile.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 = sortify(theme_name.lower())
+                        tag.save()
+                    themes.append(tag)
+                if not themes:
+                    continue
+
+                text = fragment.to_string()
+                short_text = truncate_html_words(text, 15)
+                if text == short_text:
+                    short_text = ''
+                new_fragment = Fragment.objects.create(anchor=fragment.id, 
+                        book=book, text=text, short_text=short_text)
+
+                new_fragment.save()
+                new_fragment.tags = set(meta_tags + themes + [book_tag] + ancestor_tags)
+            book.html_built.send(sender=book)
+            return True
+        return False
 
 
 class OverwritingFieldFile(FieldFile):
 
 
 class OverwritingFieldFile(FieldFile):
@@ -28,7 +202,14 @@ class OverwritingFileField(models.FileField):
 try:
     # check for south
     from south.modelsinspector import add_introspection_rules
 try:
     # check for south
     from south.modelsinspector import add_introspection_rules
-
-    add_introspection_rules([], ["^catalogue\.fields\.OverwritingFileField"])
 except ImportError:
     pass
 except ImportError:
     pass
+else:
+    add_introspection_rules([
+        (
+            [EbookField],
+            [],
+            {'format_name': ('format_name', {})}
+        )
+    ], ["^catalogue\.fields\.EbookField"])
+    add_introspection_rules([], ["^catalogue\.fields\.OverwritingFileField"])
index b323edc..637e214 100644 (file)
@@ -22,14 +22,9 @@ class Command(BaseCommand):
             help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
         make_option('-f', '--force', action='store_true', dest='force', default=False,
             help='Overwrite works already in the catalogue'),
             help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
         make_option('-f', '--force', action='store_true', dest='force', default=False,
             help='Overwrite works already in the catalogue'),
-        make_option('-E', '--no-build-epub', action='store_false', dest='build_epub', default=True,
-            help='Don\'t build EPUB file'),
-        make_option('-M', '--no-build-mobi', action='store_false', dest='build_mobi', default=True,
-            help='Don\'t build MOBI file'),
-        make_option('-T', '--no-build-txt', action='store_false', dest='build_txt', default=True,
-            help='Don\'t build TXT file'),
-        make_option('-P', '--no-build-pdf', action='store_false', dest='build_pdf', default=True,
-            help='Don\'t build PDF file'),
+        make_option('-D', '--dont-build', dest='dont_build',
+            metavar="FORMAT,...",
+            help="Skip building specified formats"),
         make_option('-S', '--no-search-index', action='store_false', dest='search_index', default=True,
             help='Skip indexing imported works for search'),
         make_option('-w', '--wait-until', dest='wait_until', metavar='TIME',
         make_option('-S', '--no-search-index', action='store_false', dest='search_index', default=True,
             help='Skip indexing imported works for search'),
         make_option('-w', '--wait-until', dest='wait_until', metavar='TIME',
@@ -42,22 +37,25 @@ class Command(BaseCommand):
 
     def import_book(self, file_path, options):
         verbose = options.get('verbose')
 
     def import_book(self, file_path, options):
         verbose = options.get('verbose')
+        if options.get('dont_build'):
+            dont_build = options.get('dont_build').lower().split(',')
+        else:
+            dont_build = None
         file_base, ext = os.path.splitext(file_path)
         book = Book.from_xml_file(file_path, overwrite=options.get('force'),
         file_base, ext = os.path.splitext(file_path)
         book = Book.from_xml_file(file_path, overwrite=options.get('force'),
-                                                    build_epub=options.get('build_epub'),
-                                                    build_txt=options.get('build_txt'),
-                                                    build_pdf=options.get('build_pdf'),
-                                                    build_mobi=options.get('build_mobi'),
-                                                    search_index=options.get('search_index'),
-                                                    search_index_reuse=True, search_index_tags=False)
+                                  dont_build=dont_build,
+                                  search_index=options.get('search_index'),
+                                  search_index_reuse=True,
+                                  search_index_tags=False)
         for ebook_format in Book.ebook_formats:
             if os.path.isfile(file_base + '.' + ebook_format):
                 getattr(book, '%s_file' % ebook_format).save(
                     '%s.%s' % (book.slug, ebook_format), 
         for ebook_format in Book.ebook_formats:
             if os.path.isfile(file_base + '.' + ebook_format):
                 getattr(book, '%s_file' % ebook_format).save(
                     '%s.%s' % (book.slug, ebook_format), 
-                    File(file(file_base + '.' + ebook_format)))
+                    File(file(file_base + '.' + ebook_format)),
+                    save=False
+                    )
                 if verbose:
                     print "Importing %s.%s" % (file_base, ebook_format)
                 if verbose:
                     print "Importing %s.%s" % (file_base, ebook_format)
-
         book.save()
 
     def import_picture(self, file_path, options):
         book.save()
 
     def import_picture(self, file_path, options):
diff --git a/apps/catalogue/migrations/0005_auto__chg_field_book_pdf_file__chg_field_book_html_file__chg_field_boo.py b/apps/catalogue/migrations/0005_auto__chg_field_book_pdf_file__chg_field_book_html_file__chg_field_boo.py
new file mode 100644 (file)
index 0000000..6f6bc81
--- /dev/null
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+
+        # Changing field 'Book.pdf_file'
+        db.alter_column('catalogue_book', 'pdf_file', self.gf('catalogue.fields.EbookField')(max_length=100, format_name='pdf'))
+
+        # Changing field 'Book.html_file'
+        db.alter_column('catalogue_book', 'html_file', self.gf('catalogue.fields.EbookField')(max_length=100, format_name='html'))
+
+        # Changing field 'Book.xml_file'
+        db.alter_column('catalogue_book', 'xml_file', self.gf('catalogue.fields.EbookField')(max_length=100, format_name='xml'))
+
+        # Changing field 'Book.txt_file'
+        db.alter_column('catalogue_book', 'txt_file', self.gf('catalogue.fields.EbookField')(max_length=100, format_name='txt'))
+
+        # Changing field 'Book.fb2_file'
+        db.alter_column('catalogue_book', 'fb2_file', self.gf('catalogue.fields.EbookField')(max_length=100, format_name='fb2'))
+
+        # Changing field 'Book.mobi_file'
+        db.alter_column('catalogue_book', 'mobi_file', self.gf('catalogue.fields.EbookField')(max_length=100, format_name='mobi'))
+
+        # Changing field 'Book.epub_file'
+        db.alter_column('catalogue_book', 'epub_file', self.gf('catalogue.fields.EbookField')(max_length=100, format_name='epub'))
+
+        # Changing field 'Book.cover'
+        db.alter_column('catalogue_book', 'cover', self.gf('catalogue.fields.EbookField')(max_length=100, null=True, format_name='cover'))
+
+    def backwards(self, orm):
+
+        # Changing field 'Book.pdf_file'
+        db.alter_column('catalogue_book', 'pdf_file', self.gf('django.db.models.fields.files.FileField')(max_length=100))
+
+        # Changing field 'Book.html_file'
+        db.alter_column('catalogue_book', 'html_file', self.gf('django.db.models.fields.files.FileField')(max_length=100))
+
+        # Changing field 'Book.xml_file'
+        db.alter_column('catalogue_book', 'xml_file', self.gf('django.db.models.fields.files.FileField')(max_length=100))
+
+        # Changing field 'Book.txt_file'
+        db.alter_column('catalogue_book', 'txt_file', self.gf('django.db.models.fields.files.FileField')(max_length=100))
+
+        # Changing field 'Book.fb2_file'
+        db.alter_column('catalogue_book', 'fb2_file', self.gf('django.db.models.fields.files.FileField')(max_length=100))
+
+        # Changing field 'Book.mobi_file'
+        db.alter_column('catalogue_book', 'mobi_file', self.gf('django.db.models.fields.files.FileField')(max_length=100))
+
+        # Changing field 'Book.epub_file'
+        db.alter_column('catalogue_book', 'epub_file', self.gf('django.db.models.fields.files.FileField')(max_length=100))
+
+        # Changing field 'Book.cover'
+        db.alter_column('catalogue_book', 'cover', self.gf('django.db.models.fields.files.FileField')(max_length=100, null=True))
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "('sort_key',)", 'object_name': 'Book'},
+            '_related_info': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
+            'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'common_slug': ('django.db.models.fields.SlugField', [], {'max_length': '120'}),
+            'cover': ('catalogue.fields.EbookField', [], {'max_length': '100', 'null': 'True', 'format_name': "'cover'", 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'epub_file': ('catalogue.fields.EbookField', [], {'default': "''", 'max_length': '100', 'format_name': "'epub'", 'blank': 'True'}),
+            'extra_info': ('jsonfield.fields.JSONField', [], {'default': "'{}'"}),
+            'fb2_file': ('catalogue.fields.EbookField', [], {'default': "''", 'max_length': '100', 'format_name': "'fb2'", 'blank': 'True'}),
+            'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}),
+            'html_file': ('catalogue.fields.EbookField', [], {'default': "''", 'max_length': '100', 'format_name': "'html'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}),
+            'mobi_file': ('catalogue.fields.EbookField', [], {'default': "''", 'max_length': '100', 'format_name': "'mobi'", 'blank': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'pdf_file': ('catalogue.fields.EbookField', [], {'default': "''", 'max_length': '100', 'format_name': "'pdf'", 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '120'}),
+            'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
+            'txt_file': ('catalogue.fields.EbookField', [], {'default': "''", 'max_length': '100', 'format_name': "'txt'", 'blank': 'True'}),
+            'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}),
+            'xml_file': ('catalogue.fields.EbookField', [], {'default': "''", 'max_length': '100', 'format_name': "'xml'", 'blank': 'True'})
+        },
+        'catalogue.bookmedia': {
+            'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}),
+            'extra_info': ('jsonfield.fields.JSONField', [], {'default': "'{}'"}),
+            'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}),
+            'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}),
+            'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+        },
+        'catalogue.collection': {
+            'Meta': {'ordering': "('title',)", 'object_name': 'Collection'},
+            'book_slugs': ('django.db.models.fields.TextField', [], {}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'primary_key': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'})
+        },
+        'catalogue.fragment': {
+            'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'},
+            'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'short_text': ('django.db.models.fields.TextField', [], {}),
+            'text': ('django.db.models.fields.TextField', [], {})
+        },
+        'catalogue.tag': {
+            'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'},
+            'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
+            'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120'}),
+            'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'})
+        },
+        'catalogue.tagrelation': {
+            'Meta': {'unique_together': "(('tag', 'content_type', 'object_id'),)", 'object_name': 'TagRelation', 'db_table': "'catalogue_tag_relation'"},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['catalogue.Tag']"})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
\ No newline at end of file
index 5672853..e6f08a9 100644 (file)
@@ -3,7 +3,7 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 import re
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 import re
-from django.conf import settings
+from django.conf import settings as settings
 from django.core.cache import get_cache
 from django.db import models
 from django.db.models import permalink
 from django.core.cache import get_cache
 from django.db import models
 from django.db.models import permalink
@@ -11,8 +11,11 @@ import django.dispatch
 from django.utils.datastructures import SortedDict
 from django.utils.translation import ugettext_lazy as _
 import jsonfield
 from django.utils.datastructures import SortedDict
 from django.utils.translation import ugettext_lazy as _
 import jsonfield
+from catalogue import constants
+from catalogue.fields import EbookField
 from catalogue.models import Tag, Fragment, BookMedia
 from catalogue.models import Tag, Fragment, BookMedia
-from catalogue.utils import create_zip, split_tags, truncate_html_words, book_upload_path
+from catalogue.utils import create_zip, split_tags, book_upload_path
+from catalogue import app_settings
 from catalogue import tasks
 from newtagging import managers
 
 from catalogue import tasks
 from newtagging import managers
 
@@ -28,7 +31,7 @@ class Book(models.Model):
             unique=True)
     common_slug = models.SlugField(_('slug'), max_length=120, db_index=True)
     language = models.CharField(_('language code'), max_length=3, db_index=True,
             unique=True)
     common_slug = models.SlugField(_('slug'), max_length=120, db_index=True)
     language = models.CharField(_('language code'), max_length=3, db_index=True,
-                    default=settings.CATALOGUE_DEFAULT_LANGUAGE)
+                    default=app_settings.DEFAULT_LANGUAGE)
     description   = models.TextField(_('description'), blank=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)
     description   = models.TextField(_('description'), blank=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)
@@ -38,12 +41,13 @@ class Book(models.Model):
     wiki_link     = models.CharField(blank=True, max_length=240)
     # files generated during publication
 
     wiki_link     = models.CharField(blank=True, max_length=240)
     # files generated during publication
 
-    cover = models.FileField(_('cover'), upload_to=book_upload_path('png'),
-                null=True, blank=True)
-    ebook_formats = ['pdf', 'epub', 'mobi', 'fb2', 'txt']
+    cover = EbookField('cover', _('cover'),
+                upload_to=book_upload_path('jpg'), null=True, blank=True)
+    ebook_formats = constants.EBOOK_FORMATS
     formats = ebook_formats + ['html', 'xml']
 
     formats = ebook_formats + ['html', 'xml']
 
-    parent        = models.ForeignKey('self', blank=True, null=True, related_name='children')
+    parent = models.ForeignKey('self', blank=True, null=True,
+        related_name='children')
 
     _related_info = jsonfield.JSONField(blank=True, null=True, editable=False)
 
 
     _related_info = jsonfield.JSONField(blank=True, null=True, editable=False)
 
@@ -152,106 +156,33 @@ class Book(models.Model):
     has_daisy_file.short_description = 'DAISY'
     has_daisy_file.boolean = True
 
     has_daisy_file.short_description = 'DAISY'
     has_daisy_file.boolean = True
 
-    def wldocument(self, parse_dublincore=True):
+    def wldocument(self, parse_dublincore=True, inherit=True):
         from catalogue.import_utils import ORMDocProvider
         from librarian.parser import WLDocument
 
         from catalogue.import_utils import ORMDocProvider
         from librarian.parser import WLDocument
 
+        if inherit and self.parent:
+            meta_fallbacks = self.parent.cover_info()
+        else:
+            meta_fallbacks = None
+
         return WLDocument.from_file(self.xml_file.path,
                 provider=ORMDocProvider(self),
         return WLDocument.from_file(self.xml_file.path,
                 provider=ORMDocProvider(self),
-                parse_dublincore=parse_dublincore)
-
-    def build_html(self):
-        from django.core.files.base import ContentFile
-        from slughifi import slughifi
-        from sortify import sortify
-        from librarian import html
-
-        meta_tags = list(self.tags.filter(
-            category__in=('author', 'epoch', 'genre', 'kind')))
-        book_tag = self.book_tag()
-
-        html_output = self.wldocument(parse_dublincore=False).as_html()
-        if html_output:
-            self.html_file.save('%s.html' % self.slug,
-                    ContentFile(html_output.get_string()))
-
-            # 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 = sortify(theme_name.lower())
-                        tag.save()
-                    themes.append(tag)
-                if not themes:
-                    continue
-
-                text = fragment.to_string()
-                short_text = truncate_html_words(text, 15)
-                if text == short_text:
-                    short_text = ''
-                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()
-            self.html_built.send(sender=self)
-            return True
-        return False
-
-    # Thin wrappers for builder tasks
-    def build_cover(self):
-        """(Re)builds the cover image."""
-        return tasks.build_cover.delay(self.pk)
-    def build_pdf(self, *args, **kwargs):
-        """(Re)builds PDF."""
-        return tasks.build_pdf.delay(self.pk, *args, **kwargs)
-    def build_epub(self, *args, **kwargs):
-        """(Re)builds EPUB."""
-        return tasks.build_epub.delay(self.pk, *args, **kwargs)
-    def build_mobi(self, *args, **kwargs):
-        """(Re)builds MOBI."""
-        return tasks.build_mobi.delay(self.pk, *args, **kwargs)
-    def build_fb2(self, *args, **kwargs):
-        """(Re)build FB2"""
-        return tasks.build_fb2.delay(self.pk, *args, **kwargs)
-    def build_txt(self, *args, **kwargs):
-        """(Re)builds TXT."""
-        return tasks.build_txt.delay(self.pk, *args, **kwargs)
+                parse_dublincore=parse_dublincore,
+                meta_fallbacks=meta_fallbacks)
 
     @staticmethod
     def zip_format(format_):
         def pretty_file_name(book):
             return "%s/%s.%s" % (
 
     @staticmethod
     def zip_format(format_):
         def pretty_file_name(book):
             return "%s/%s.%s" % (
-                b.extra_info['author'],
-                b.slug,
+                book.extra_info['author'],
+                book.slug,
                 format_)
 
         field_name = "%s_file" % format_
         books = Book.objects.filter(parent=None).exclude(**{field_name: ""})
         paths = [(pretty_file_name(b), getattr(b, field_name).path)
                     for b in books.iterator()]
                 format_)
 
         field_name = "%s_file" % format_
         books = Book.objects.filter(parent=None).exclude(**{field_name: ""})
         paths = [(pretty_file_name(b), getattr(b, field_name).path)
                     for b in books.iterator()]
-        return create_zip(paths,
-                    getattr(settings, "ALL_%s_ZIP" % format_.upper()))
+        return create_zip(paths, app_settings.FORMAT_ZIPS[format_])
 
     def zip_audiobooks(self, format_):
         bm = BookMedia.objects.filter(book=self, type=format_)
 
     def zip_audiobooks(self, format_):
         bm = BookMedia.objects.filter(book=self, type=format_)
@@ -291,8 +222,11 @@ class Book(models.Model):
 
     @classmethod
     def from_text_and_meta(cls, raw_file, book_info, overwrite=False,
 
     @classmethod
     def from_text_and_meta(cls, raw_file, book_info, overwrite=False,
-            build_epub=True, build_txt=True, build_pdf=True, build_mobi=True, build_fb2=True,
-            search_index=True, search_index_tags=True, search_index_reuse=False):
+            dont_build=None, search_index=True,
+            search_index_tags=True, search_index_reuse=False):
+        if dont_build is None:
+            dont_build = set()
+        dont_build = set.union(set(dont_build), set(app_settings.DONT_BUILD))
 
         # check for parts before we do anything
         children = []
 
         # check for parts before we do anything
         children = []
@@ -312,12 +246,17 @@ class Book(models.Model):
 
         if created:
             book_shelves = []
 
         if created:
             book_shelves = []
+            old_cover = None
         else:
             if not overwrite:
                 raise Book.AlreadyExists(_('Book %s already exists') % (
                         book_slug))
             # Save shelves for this book
             book_shelves = list(book.tags.filter(category='set'))
         else:
             if not overwrite:
                 raise Book.AlreadyExists(_('Book %s already exists') % (
                         book_slug))
             # Save shelves for this book
             book_shelves = list(book.tags.filter(category='set'))
+            old_cover = book.cover_info()
+
+        # Save XML file
+        book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
 
         book.language = book_info.language
         book.title = book_info.title
 
         book.language = book_info.language
         book.title = book_info.title
@@ -332,44 +271,41 @@ class Book(models.Model):
 
         book.tags = set(meta_tags + book_shelves)
 
 
         book.tags = set(meta_tags + book_shelves)
 
-        obsolete_children = set(b for b in book.children.all() if b not in children)
+        cover_changed = old_cover != book.cover_info()
+        obsolete_children = set(b for b in book.children.all()
+                                if b not in children)
         for n, child_book in enumerate(children):
             child_book.parent = book
             child_book.parent_number = n
             child_book.save()
         for n, child_book in enumerate(children):
             child_book.parent = book
             child_book.parent_number = n
             child_book.save()
+            if cover_changed:
+                child_book.parent_cover_changed()
         # Disown unfaithful children and let them cope on their own.
         for child in obsolete_children:
             child.parent = None
             child.parent_number = 0
             child.save()
             tasks.fix_tree_tags.delay(child)
         # Disown unfaithful children and let them cope on their own.
         for child in obsolete_children:
             child.parent = None
             child.parent_number = 0
             child.save()
             tasks.fix_tree_tags.delay(child)
-
-        # Save XML file
-        book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
+            if old_cover:
+                child.parent_cover_changed()
 
         # delete old fragments when overwriting
         book.fragments.all().delete()
         # Build HTML, fix the tree tags, build cover.
 
         # delete old fragments when overwriting
         book.fragments.all().delete()
         # Build HTML, fix the tree tags, build cover.
-        has_own_text = bool(book.build_html())
+        has_own_text = bool(book.html_file.build())
         tasks.fix_tree_tags.delay(book)
         tasks.fix_tree_tags.delay(book)
-        book.build_cover(book_info)
+        if 'cover' not in dont_build:
+            book.cover.build_delay()
         
         # No saves behind this point.
 
         if has_own_text:
         
         # No saves behind this point.
 
         if has_own_text:
-            if not settings.NO_BUILD_TXT and build_txt:
-                book.build_txt()
-            if not settings.NO_BUILD_FB2 and build_fb2:
-                book.build_fb2()
-
-        if not settings.NO_BUILD_EPUB and build_epub:
-            book.build_epub()
-
-        if not settings.NO_BUILD_PDF and build_pdf:
-            book.build_pdf()
-
-        if not settings.NO_BUILD_MOBI and build_mobi:
-            book.build_mobi()
+            for format_ in constants.EBOOK_FORMATS_WITHOUT_CHILDREN:
+                if format_ not in dont_build:
+                    getattr(book, '%s_file' % format_).build_delay()
+        for format_ in constants.EBOOK_FORMATS_WITH_CHILDREN:
+            if format_ not in dont_build:
+                getattr(book, '%s_file' % format_).build_delay()
 
         if not settings.NO_SEARCH_INDEX and search_index:
             book.search_index(index_tags=search_index_tags, reuse_index=search_index_reuse)
 
         if not settings.NO_SEARCH_INDEX and search_index:
             book.search_index(index_tags=search_index_tags, reuse_index=search_index_reuse)
@@ -394,7 +330,8 @@ class Book(models.Model):
             sub_parent_tags = parent_tags + [book.book_tag()]
             for frag in book.fragments.all():
                 affected_tags.update(frag.tags)
             sub_parent_tags = parent_tags + [book.book_tag()]
             for frag in book.fragments.all():
                 affected_tags.update(frag.tags)
-                frag.tags = list(frag.tags.exclude(category='book')) + sub_parent_tags
+                frag.tags = list(frag.tags.exclude(category='book')
+                                    ) + sub_parent_tags
             for child in book.children.all():
                 affected_tags.update(fix_subtree(child, sub_parent_tags))
             return affected_tags
             for child in book.children.all():
                 affected_tags.update(fix_subtree(child, sub_parent_tags))
             return affected_tags
@@ -415,6 +352,36 @@ class Book(models.Model):
             book.reset_theme_counter()
             book = book.parent
 
             book.reset_theme_counter()
             book = book.parent
 
+    def cover_info(self, inherit=True):
+        """Returns a dictionary to serve as fallback for BookInfo.
+
+        For now, the only thing inherited is the cover image.
+        """
+        need = False
+        info = {}
+        for field in ('cover_url', 'cover_by', 'cover_source'):
+            val = self.extra_info.get(field)
+            if val:
+                info[field] = val
+            else:
+                need = True
+        if inherit and need and self.parent is not None:
+            parent_info = self.parent.cover_info()
+            parent_info.update(info)
+            info = parent_info
+        return info
+
+    def parent_cover_changed(self):
+        """Called when parent book's cover image is changed."""
+        if not self.cover_info(inherit=False):
+            if 'cover' not in app_settings.DONT_BUILD:
+                self.cover.build_delay()
+            for format_ in constants.EBOOK_FORMATS_WITH_COVERS:
+                if format_ not in app_settings.DONT_BUILD:
+                    getattr(self, '%s_file' % format_).build_delay()
+            for child in self.children.all():
+                child.parent_cover_changed()
+
     def related_info(self):
         """Keeps info about related objects (tags, media) in cache field."""
         if self._related_info is not None:
     def related_info(self):
         """Keeps info about related objects (tags, media) in cache field."""
         if self._related_info is not None:
@@ -607,20 +574,9 @@ class Book(models.Model):
             return None
 
 
             return None
 
 
-def _has_factory(ftype):
-    has = lambda self: bool(getattr(self, "%s_file" % ftype))
-    has.short_description = ftype.upper()
-    has.__doc__ = None
-    has.boolean = True
-    has.__name__ = "has_%s_file" % ftype
-    return has
-
-    
 # add the file fields
 # add the file fields
-for t in Book.formats:
-    field_name = "%s_file" % t
-    models.FileField(_("%s file" % t.upper()),
-            upload_to=book_upload_path(t),
-            blank=True).contribute_to_class(Book, field_name)
-
-    setattr(Book, "has_%s_file" % t, _has_factory(t))
+for format_ in Book.formats:
+    field_name = "%s_file" % format_
+    EbookField(format_, _("%s file" % format_.upper()),
+            upload_to=book_upload_path(format_),
+            blank=True, default='').contribute_to_class(Book, field_name)
index f1d8a90..af00c35 100644 (file)
@@ -34,75 +34,6 @@ def index_book(book_id, book_info=None):
         raise e
 
 
         raise e
 
 
-def _build_ebook(book_id, ext, transform):
-    """Generic ebook builder."""
-    from django.core.files import File
-    from catalogue.models import Book
-
-    book = Book.objects.get(pk=book_id)
-    out = transform(book.wldocument())
-    field_name = '%s_file' % ext
-    # Update instead of saving the model to avoid race condition.
-    getattr(book, field_name).save('%s.%s' % (book.slug, ext),
-            File(open(out.get_filename())),
-            save=False
-        )
-    Book.objects.filter(pk=book_id).update(**{
-            field_name: getattr(book, field_name)
-        })
-
-
-@task(ignore_result=True)
-def build_txt(book_id):
-    """(Re)builds the TXT file for a book."""
-    _build_ebook(book_id, 'txt', lambda doc: doc.as_text())
-
-
-@task(ignore_result=True, rate_limit=settings.CATALOGUE_PDF_RATE_LIMIT)
-def build_pdf(book_id):
-    """(Re)builds the pdf file for a book."""
-    from catalogue.models import Book
-    from catalogue.utils import remove_zip
-    from waiter.utils import clear_cache
-
-    _build_ebook(book_id, 'pdf',
-        lambda doc: doc.as_pdf(morefloats=settings.LIBRARIAN_PDF_MOREFLOATS))
-    # Remove cached downloadables
-    remove_zip(settings.ALL_PDF_ZIP)
-    book = Book.objects.get(pk=book_id)
-    clear_cache(book.slug)
-
-
-@task(ignore_result=True, rate_limit=settings.CATALOGUE_EPUB_RATE_LIMIT)
-def build_epub(book_id):
-    """(Re)builds the EPUB file for a book."""
-    from catalogue.utils import remove_zip
-
-    _build_ebook(book_id, 'epub', lambda doc: doc.as_epub())
-    # remove zip with all epub files
-    remove_zip(settings.ALL_EPUB_ZIP)
-
-
-@task(ignore_result=True, rate_limit=settings.CATALOGUE_MOBI_RATE_LIMIT)
-def build_mobi(book_id):
-    """(Re)builds the MOBI file for a book."""
-    from catalogue.utils import remove_zip
-
-    _build_ebook(book_id, 'mobi', lambda doc: doc.as_mobi())
-    # remove zip with all mobi files
-    remove_zip(settings.ALL_MOBI_ZIP)
-
-
-@task(ignore_result=True, rate_limit=settings.CATALOGUE_FB2_RATE_LIMIT)
-def build_fb2(book_id, *args, **kwargs):
-    """(Re)builds the FB2 file for a book."""
-    from catalogue.utils import remove_zip
-
-    _build_ebook(book_id, 'fb2', lambda doc: doc.as_fb2())
-    # remove zip with all fb2 files
-    remove_zip(settings.ALL_FB2_ZIP)
-
-
 @task(ignore_result=True, rate_limit=settings.CATALOGUE_CUSTOMPDF_RATE_LIMIT)
 def build_custom_pdf(book_id, customizations, file_name):
     """Builds a custom PDF file."""
 @task(ignore_result=True, rate_limit=settings.CATALOGUE_CUSTOMPDF_RATE_LIMIT)
 def build_custom_pdf(book_id, customizations, file_name):
     """Builds a custom PDF file."""
@@ -116,20 +47,3 @@ def build_custom_pdf(book_id, customizations, file_name):
                 customizations=customizations,
                 morefloats=settings.LIBRARIAN_PDF_MOREFLOATS)
         DefaultStorage().save(file_name, File(open(pdf.get_filename())))
                 customizations=customizations,
                 morefloats=settings.LIBRARIAN_PDF_MOREFLOATS)
         DefaultStorage().save(file_name, File(open(pdf.get_filename())))
-
-
-@task(ignore_result=True)
-def build_cover(book_id):
-    """(Re)builds the cover image."""
-    from StringIO import StringIO
-    from django.core.files.base import ContentFile
-    from librarian.cover import WLCover
-    from catalogue.models import Book
-
-    book = Book.objects.get(pk=book_id)
-    book_info = book.wldocument().book_info
-    cover = WLCover(book_info).image()
-    imgstr = StringIO()
-    cover.save(imgstr, 'png')
-    book.cover.save(None, ContentFile(imgstr.getvalue()), save=False)
-    Book.objects.filter(pk=book_id).update(cover=book.cover)
index f42818f..59e5138 100644 (file)
@@ -1,31 +1,30 @@
-from django.conf import settings
+# -*- 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 django.test import TestCase
 from django.test import TestCase
-import shutil
+from django.test.utils import override_settings
 import tempfile
 from slughifi import slughifi
 from librarian import WLURI
 
 import tempfile
 from slughifi import slughifi
 from librarian import WLURI
 
+@override_settings(
+    MEDIA_ROOT=tempfile.mkdtemp(prefix='djangotest_'),
+    CATALOGUE_DONT_BUILD={'pdf', 'mobi', 'epub', 'txt', 'fb2', 'cover'},
+    NO_SEARCH_INDEX = True,
+    CELERY_ALWAYS_EAGER = True,
+    CACHES={
+            'api': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
+            'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
+            'permanent': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
+        },
+)
 class WLTestCase(TestCase):
     """
         Generic base class for tests. Adds settings freeze and clears MEDIA_ROOT.
     """
     longMessage = True
 
 class WLTestCase(TestCase):
     """
         Generic base class for tests. Adds settings freeze and clears MEDIA_ROOT.
     """
     longMessage = True
 
-    def setUp(self):
-        self._MEDIA_ROOT, settings.MEDIA_ROOT = settings.MEDIA_ROOT, tempfile.mkdtemp(prefix='djangotest_')
-        settings.NO_SEARCH_INDEX = settings.NO_BUILD_PDF = settings.NO_BUILD_MOBI = settings.NO_BUILD_EPUB = settings.NO_BUILD_TXT = settings.NO_BUILD_FB2 = True
-        settings.CELERY_ALWAYS_EAGER = True
-        self._CACHES, settings.CACHES = settings.CACHES, {
-            'default': {
-                'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
-            }
-        }
-
-    def tearDown(self):
-        shutil.rmtree(settings.MEDIA_ROOT, True)
-        settings.MEDIA_ROOT = self._MEDIA_ROOT
-        settings.CACHES = self._CACHES
-
 
 class PersonStub(object):
 
 
 class PersonStub(object):
 
index 09d0e1e..76061d0 100644 (file)
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-from __future__ import with_statement
+from django.conf import settings
 
 from django.core.files.base import ContentFile, File
 from catalogue.test_utils import *
 
 from django.core.files.base import ContentFile, File
 from catalogue.test_utils import *
@@ -418,7 +418,7 @@ class BookImportGenerateTest(WLTestCase):
         self.book = models.Book.from_xml_file(xml)
 
     def test_gen_pdf(self):
         self.book = models.Book.from_xml_file(xml)
 
     def test_gen_pdf(self):
-        self.book.build_pdf()
+        self.book.pdf_file.build()
         book = models.Book.objects.get(pk=self.book.pk)
         self.assertTrue(path.exists(book.pdf_file.path))
 
         book = models.Book.objects.get(pk=self.book.pk)
         self.assertTrue(path.exists(book.pdf_file.path))
 
@@ -426,7 +426,7 @@ class BookImportGenerateTest(WLTestCase):
         """This book contains a child."""
         xml = path.join(path.dirname(__file__), "files/fraszki.xml")
         parent = models.Book.from_xml_file(xml)
         """This book contains a child."""
         xml = path.join(path.dirname(__file__), "files/fraszki.xml")
         parent = models.Book.from_xml_file(xml)
-        parent.build_pdf()
+        parent.pdf_file.build()
         parent = models.Book.objects.get(pk=parent.pk)
         self.assertTrue(path.exists(parent.pdf_file.path))
 
         parent = models.Book.objects.get(pk=parent.pk)
         self.assertTrue(path.exists(parent.pdf_file.path))
 
index 5d2ba66..da427e8 100644 (file)
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
+from django.conf import settings
 from os.path import basename, exists, join, dirname
 from django.core.files.base import ContentFile, File
 
 from os.path import basename, exists, join, dirname
 from django.core.files.base import ContentFile, File
 
index bda7e04..1ac0ee8 100644 (file)
@@ -276,3 +276,27 @@ def clear_custom_pdf(book):
     """
     from waiter.utils import clear_cache
     clear_cache('book/%s' % book.slug)
     """
     from waiter.utils import clear_cache
     clear_cache('book/%s' % book.slug)
+
+
+class AppSettings(object):
+    """Allows specyfying custom settings for an app, with default values.
+
+    Just subclass, set some properties and instantiate with a prefix.
+    Getting a SETTING from an instance will check for prefix_SETTING
+    in project settings if set, else take the default. The value will be
+    then filtered through _more_SETTING method, if there is one.
+
+    """
+    def __init__(self, prefix):
+        self._prefix = prefix
+
+    def __getattribute__(self, name):
+        if name.startswith('_'):
+            return object.__getattribute__(self, name)
+        value = getattr(settings,
+                         "%s_%s" % (self._prefix, name),
+                         object.__getattribute__(self, name))
+        more = "_more_%s" % name
+        if hasattr(self, more):
+            value = getattr(self, more)(value)
+        return value
index 202acdd..785aa6d 100644 (file)
@@ -2,11 +2,11 @@
 from __future__ import with_statement
 
 from os import path
 from __future__ import with_statement
 
 from os import path
-from django.test import TestCase
 from picture.models import Picture
 from picture.models import Picture
+from catalogue.test_utils import WLTestCase
 
 
 
 
-class PictureTest(TestCase):
+class PictureTest(WLTestCase):
     
     def test_import(self):
         picture = Picture.from_xml_file(path.join(path.dirname(__file__), "files/kandinsky-composition-viii.xml"))
     
     def test_import(self):
         picture = Picture.from_xml_file(path.join(path.dirname(__file__), "files/kandinsky-composition-viii.xml"))
index 7382888..5155a84 100644 (file)
@@ -1,33 +1,24 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-
-from __future__ import with_statement
-
 from django.conf import settings
 from django.conf import settings
-from search import Index, Search, IndexStore, JVM, SearchResult
-from catalogue import models
+from django.test.utils import override_settings
 from catalogue.test_utils import WLTestCase
 from lucene import PolishAnalyzer, Version
 from catalogue.test_utils import WLTestCase
 from lucene import PolishAnalyzer, Version
-#from nose.tools import raises
 from os import path
 from os import path
+import tempfile
+from catalogue import models
+from search import Search, SearchResult
 
 
 
 
+@override_settings(
+    SEARCH_INDEX = tempfile.mkdtemp(prefix='djangotest_search_'),
+)
 class BookSearchTests(WLTestCase):
     def setUp(self):
 class BookSearchTests(WLTestCase):
     def setUp(self):
-        JVM.attachCurrentThread()
         WLTestCase.setUp(self)
         WLTestCase.setUp(self)
-        settings.NO_SEARCH_INDEX = False
-        settings.SEARCH_INDEX = path.join(settings.MEDIA_ROOT, 'search')
 
         txt = path.join(path.dirname(__file__), 'files/fraszka-do-anusie.xml')
 
         txt = path.join(path.dirname(__file__), 'files/fraszka-do-anusie.xml')
-        self.book = models.Book.from_xml_file(txt)
-
-        index = Index()
-        index.open()
-        try:
-            index.index_book(self.book)
-        except:
-            index.close()
-
+        with self.settings(NO_SEARCH_INDEX=False):
+            self.book = models.Book.from_xml_file(txt)
         self.search = Search()
 
     def test_search_perfect_book_author(self):
         self.search = Search()
 
     def test_search_perfect_book_author(self):
index 17a9ed3..cbe81ee 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 17a9ed3b7ef12e0786ddf46bf8a52b1087224762
+Subproject commit cbe81ee35b07783b4f52c3d3dda83db7aaf82d34
index 6bf5a88..446c730 100644 (file)
@@ -5,29 +5,12 @@ API_WAIT = 10
 MAX_TAG_LIST = 6
 
 NO_SEARCH_INDEX = False
 MAX_TAG_LIST = 6
 
 NO_SEARCH_INDEX = False
-NO_BUILD_EPUB = False
-NO_BUILD_TXT = False
-NO_BUILD_FB2 = False
-# You'll need XeLaTeX to generate PDF files.
-NO_BUILD_PDF = True
 NO_CUSTOM_PDF = True
 NO_CUSTOM_PDF = True
-# You'll need Calibre installed to generate MOBI files.
-NO_BUILD_MOBI = True
-
-
-ALL_EPUB_ZIP = 'wolnelektury_pl_epub'
-ALL_PDF_ZIP = 'wolnelektury_pl_pdf'
-ALL_MOBI_ZIP = 'wolnelektury_pl_mobi'
-ALL_FB2_ZIP = 'wolnelektury_pl_fb2'
 
 CATALOGUE_DEFAULT_LANGUAGE = 'pol'
 PUBLISH_PLAN_FEED = 'http://redakcja.wolnelektury.pl/documents/track/editor-proofreading/?published=false'
 
 # limit rate for ebooks creation
 
 CATALOGUE_DEFAULT_LANGUAGE = 'pol'
 PUBLISH_PLAN_FEED = 'http://redakcja.wolnelektury.pl/documents/track/editor-proofreading/?published=false'
 
 # limit rate for ebooks creation
-CATALOGUE_PDF_RATE_LIMIT = '1/m'
-CATALOGUE_EPUB_RATE_LIMIT = '6/m'
-CATALOGUE_FB2_RATE_LIMIT = '5/m'
-CATALOGUE_MOBI_RATE_LIMIT = '5/m'
 CATALOGUE_CUSTOMPDF_RATE_LIMIT = '1/m'
 
 # set to 'new' or 'old' to skip time-consuming test
 CATALOGUE_CUSTOMPDF_RATE_LIMIT = '1/m'
 
 # set to 'new' or 'old' to skip time-consuming test