1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 from django.conf import settings
6 from django.core.files import File
7 from django.db import models
8 from django.db.models.fields.files import FieldFile
9 from catalogue import app_settings
10 from catalogue.utils import remove_zip, truncate_html_words
11 from celery.task import Task, task
12 from waiter.utils import clear_cache
15 class EbookFieldFile(FieldFile):
16 """Represents contents of an ebook file field."""
19 """Build the ebook immediately."""
20 return self.field.builder.build(self)
22 def build_delay(self):
23 """Builds the ebook in a delayed task."""
24 return self.field.builder.delay(self.instance, self.field.attname)
27 class EbookField(models.FileField):
28 """Represents an ebook file field, attachable to a model."""
29 attr_class = EbookFieldFile
31 def __init__(self, format_name, *args, **kwargs):
32 super(EbookField, self).__init__(*args, **kwargs)
33 self.format_name = format_name
37 """Finds a celery task suitable for the format of the field."""
38 return BuildEbook.for_format(self.format_name)
40 def contribute_to_class(self, cls, name):
41 super(EbookField, self).contribute_to_class(cls, name)
43 def has(model_instance):
44 return bool(getattr(model_instance, self.attname, None))
46 has.__name__ = "has_%s" % self.attname
47 has.short_description = self.name
49 setattr(cls, 'has_%s' % self.attname, has)
52 class BuildEbook(Task):
56 def register(cls, format_name):
57 """A decorator for registering subclasses for particular formats."""
59 cls.formats[format_name] = builder
64 def for_format(cls, format_name):
65 """Returns a celery task suitable for specified format."""
66 return cls.formats.get(format_name, BuildEbookTask)
69 def transform(wldoc, fieldfile):
70 """Transforms an librarian.WLDocument into an librarian.OutputFile.
72 By default, it just calls relevant wldoc.as_??? method.
75 return getattr(wldoc, "as_%s" % fieldfile.field.format_name)()
77 def run(self, obj, field_name):
78 """Just run `build` on FieldFile, can't pass it directly to Celery."""
79 return self.build(getattr(obj, field_name))
81 def build(self, fieldfile):
82 book = fieldfile.instance
83 out = self.transform(book.wldocument(), fieldfile)
84 fieldfile.save(None, File(open(out.get_filename())), save=False)
85 if book.pk is not None:
86 type(book).objects.filter(pk=book.pk).update(**{
87 fieldfile.field.attname: fieldfile
89 if fieldfile.field.format_name in app_settings.FORMAT_ZIPS:
90 remove_zip(app_settings.FORMAT_ZIPS[fieldfile.field.format_name])
91 # Don't decorate BuildEbook, because we want to subclass it.
92 BuildEbookTask = task(BuildEbook, ignore_result=True)
95 @BuildEbook.register('txt')
96 @task(ignore_result=True)
97 class BuildTxt(BuildEbook):
99 def transform(wldoc, fieldfile):
100 return wldoc.as_text()
103 @BuildEbook.register('pdf')
104 @task(ignore_result=True)
105 class BuildPdf(BuildEbook):
107 def transform(wldoc, fieldfile):
108 return wldoc.as_pdf(morefloats=settings.LIBRARIAN_PDF_MOREFLOATS)
110 def build(self, fieldfile):
111 BuildEbook.build(self, fieldfile)
112 clear_cache(fieldfile.instance.slug)
115 @BuildEbook.register('html')
116 @task(ignore_result=True)
117 class BuildHtml(BuildEbook):
118 def build(self, fieldfile):
119 from django.core.files.base import ContentFile
120 from slughifi import slughifi
121 from sortify import sortify
122 from librarian import html
123 from catalogue.models import Fragment, Tag
125 book = fieldfile.instance
127 meta_tags = list(book.tags.filter(
128 category__in=('author', 'epoch', 'genre', 'kind')))
129 book_tag = book.book_tag()
131 html_output = self.transform(
132 book.wldocument(parse_dublincore=False),
135 fieldfile.save(None, ContentFile(html_output.get_string()),
137 type(book).objects.filter(pk=book.pk).update(**{
138 fieldfile.field.attname: fieldfile
141 # get ancestor l-tags for adding to new fragments
145 ancestor_tags.append(p.book_tag())
148 # Delete old fragments and create them from scratch
149 book.fragments.all().delete()
151 closed_fragments, open_fragments = html.extract_fragments(fieldfile.path)
152 for fragment in closed_fragments.values():
154 theme_names = [s.strip() for s in fragment.themes.split(',')]
155 except AttributeError:
158 for theme_name in theme_names:
161 tag, created = Tag.objects.get_or_create(
162 slug=slughifi(theme_name),
165 tag.name = theme_name
166 tag.sort_key = sortify(theme_name.lower())
172 text = fragment.to_string()
173 short_text = truncate_html_words(text, 15)
174 if text == short_text:
176 new_fragment = Fragment.objects.create(anchor=fragment.id,
177 book=book, text=text, short_text=short_text)
180 new_fragment.tags = set(meta_tags + themes + [book_tag] + ancestor_tags)
181 book.html_built.send(sender=book)
186 class OverwritingFieldFile(FieldFile):
188 Deletes the old file before saving the new one.
191 def save(self, name, content, *args, **kwargs):
192 leave = kwargs.pop('leave', None)
193 # delete if there's a file already and there's a new one coming
194 if not leave and self and (not hasattr(content, 'path') or
195 content.path != self.path):
196 self.delete(save=False)
197 return super(OverwritingFieldFile, self).save(
198 name, content, *args, **kwargs)
201 class OverwritingFileField(models.FileField):
202 attr_class = OverwritingFieldFile
207 from south.modelsinspector import add_introspection_rules
211 add_introspection_rules([
215 {'format_name': ('format_name', {})}
217 ], ["^catalogue\.fields\.EbookField"])
218 add_introspection_rules([], ["^catalogue\.fields\.OverwritingFileField"])