From: Marcin Koziej Date: Mon, 3 Sep 2012 14:55:43 +0000 (+0200) Subject: Merge branch 'master' into sunburnt X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/ddaff853c515ef7d188523d9ef17d271901dd581?hp=-c Merge branch 'master' into sunburnt Conflicts: apps/catalogue/management/commands/importbooks.py apps/catalogue/models/book.py --- ddaff853c515ef7d188523d9ef17d271901dd581 diff --combined apps/catalogue/management/commands/importbooks.py index 1f08f7cff,637e2148f..8f181cf98 --- a/apps/catalogue/management/commands/importbooks.py +++ b/apps/catalogue/management/commands/importbooks.py @@@ -22,14 -22,9 +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'), - 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', @@@ -42,21 -37,25 +37,23 @@@ 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'), - 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_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), - File(file(file_base + '.' + ebook_format))) + '%s.%s' % (book.slug, ebook_format), + File(file(file_base + '.' + ebook_format)), + save=False + ) if verbose: print "Importing %s.%s" % (file_base, ebook_format) - book.save() def import_picture(self, file_path, options): @@@ -81,15 -80,13 +78,15 @@@ time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(wait_until)), wait_until - time.time()) + index = None if options.get('search_index') and not settings.NO_SEARCH_INDEX: index = Index() - index.open() try: index.index_tags() - finally: - index.close() + index.index.commit() + except Exception, e: + index.index.rollback() + raise e # Start transaction management. transaction.commit_unless_managed() @@@ -98,7 -95,7 +95,7 @@@ files_imported = 0 files_skipped = 0 - + for dir_name in directories: if not os.path.isdir(dir_name): print self.style.ERROR("%s: Not a directory. Skipping." % dir_name) @@@ -129,7 -126,7 +126,7 @@@ self.import_book(file_path, options) files_imported += 1 transaction.commit() - + except (Book.AlreadyExists, Picture.AlreadyExists): print self.style.ERROR('%s: Book or Picture already imported. Skipping. To overwrite use --force.' % file_path) @@@ -161,3 -158,4 +158,3 @@@ transaction.commit() transaction.leave_transaction_management() - diff --combined apps/catalogue/models/book.py index d3df67e6f,6a31f4bff..5bc1e1021 --- a/apps/catalogue/models/book.py +++ b/apps/catalogue/models/book.py @@@ -3,7 -3,7 +3,7 @@@ # 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 @@@ -11,8 -11,11 +11,11 @@@ import django.dispatc 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.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 @@@ -28,7 -31,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, - 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) @@@ -38,12 -41,13 +41,13 @@@ 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'] - 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) @@@ -152,137 -156,53 +156,53 @@@ 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 + 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), - parse_dublincore=parse_dublincore) - - def build_cover(self, book_info=None): - """(Re)builds the cover image.""" - from StringIO import StringIO - from django.core.files.base import ContentFile - from librarian.cover import WLCover - - if book_info is None: - book_info = self.wldocument().book_info - - cover = WLCover(book_info).image() - imgstr = StringIO() - cover.save(imgstr, 'png') - self.cover.save(None, ContentFile(imgstr.getvalue())) - - 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_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" % ( - 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()] - 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_) paths = map(lambda bm: (None, bm.file.path), bm) return create_zip(paths, "%s_%s" % (self.slug, format_)) - def search_index(self, book_info=None, reuse_index=False, index_tags=True): + def search_index(self, book_info=None, index=None, index_tags=True, commit=True): import search - if reuse_index: - idx = search.ReusableIndex() - else: - idx = search.Index() - - idx.open() + if index is None: + index = search.Index() try: - idx.index_book(self, book_info) + index.index_book(self, book_info) if index_tags: idx.index_tags() - finally: - idx.close() + if commit: + index.index.commit() + except Exception, e: + index.index.rollback() + raise e + @classmethod def from_xml_file(cls, xml_file, **kwargs): @@@ -302,8 -222,11 +222,11 @@@ @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): + dont_build=None, search_index=True, - search_index_tags=True, search_index_reuse=False): ++ search_index_tags=True): + 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 = [] @@@ -315,7 -238,6 +238,6 @@@ raise Book.DoesNotExist(_('Book "%s" does not exist.') % part_url.slug) - # Read book metadata book_slug = book_info.url.slug if re.search(r'[^a-z0-9-]', book_slug): @@@ -324,12 -246,17 +246,17 @@@ 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')) + 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 @@@ -344,46 -271,51 +271,50 @@@ 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) + notify_cover_changed = [] for n, child_book in enumerate(children): + new_child = child_book.parent != book child_book.parent = book child_book.parent_number = n child_book.save() + if new_child or cover_changed: + notify_cover_changed.append(child_book) # 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 and HTML files - book.xml_file.save('%s.xml' % book.slug, raw_file, save=False) - book.build_cover(book_info) + if old_cover: + notify_cover_changed.append(child) # delete old fragments when overwriting book.fragments.all().delete() - - if book.build_html(): - # No direct saves behind this point. - if not settings.NO_BUILD_TXT and build_txt: - book.build_txt() - - 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() - - if not settings.NO_BUILD_FB2 and build_fb2: - book.build_fb2() + # Build HTML, fix the tree tags, build cover. + has_own_text = bool(book.html_file.build()) + tasks.fix_tree_tags.delay(book) + if 'cover' not in dont_build: + book.cover.build_delay() + + # No saves behind this point. + + if has_own_text: + 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) - #index_book.delay(book.id, book_info) + tasks.index_book.delay(book.id, book_info=book_info, index_tags=search_index_tags) - tasks.fix_tree_tags.delay(book) + for child in notify_cover_changed: + child.parent_cover_changed() + cls.published.send(sender=book) return book @@@ -403,7 -335,8 +334,8 @@@ 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 @@@ -424,6 -357,36 +356,36 @@@ 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: @@@ -616,20 -579,9 +578,9 @@@ 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 - 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) diff --combined apps/catalogue/tasks.py index 8906a141c,03f4a3973..e7f7b2947 --- a/apps/catalogue/tasks.py +++ b/apps/catalogue/tasks.py @@@ -24,86 -24,17 +24,17 @@@ def fix_tree_tags(book) @task -def index_book(book_id, book_info=None): +def index_book(book_id, book_info=None, **kwargs): from catalogue.models import Book try: - return Book.objects.get(id=book_id).search_index(book_info) + return Book.objects.get(id=book_id).search_index(book_info, **kwargs) except Exception, e: print "Exception during index: %s" % e print_exc() 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(rate_limit=settings.CATALOGUE_CUSTOMPDF_RATE_LIMIT) + @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.""" from django.core.files import File @@@ -112,7 -43,14 +43,14 @@@ print "will gen %s" % DefaultStorage().path(file_name) if not DefaultStorage().exists(file_name): + kwargs = { + 'cover': True, + } + if 'no-cover' in customizations: + kwargs['cover'] = False + customizations.remove('no-cover') pdf = Book.objects.get(pk=book_id).wldocument().as_pdf( customizations=customizations, - morefloats=settings.LIBRARIAN_PDF_MOREFLOATS) + morefloats=settings.LIBRARIAN_PDF_MOREFLOATS, + **kwargs) DefaultStorage().save(file_name, File(open(pdf.get_filename()))) diff --combined requirements.txt index 53ff47d2d,9cb79727f..c7e33c2f2 --- a/requirements.txt +++ b/requirements.txt @@@ -10,7 -10,10 +10,10 @@@ django-piston<=0.2. #django-jsonfield -e git+git://github.com/bradjasper/django-jsonfield.git@2f427368ad70bf8d9a0580df58ec0eb0654d62ae#egg=django-jsonfield django-picklefield - django-allauth>=0.4,<0.5 + #django-allauth>=0.4,<0.5 + # version of django-allauth 0.4 with install script fixed + -e git+git://github.com/pennersr/django-allauth.git@3a03db9b2ecca370af228df367bd8fa52afea5ea#egg=django-allauth + django-honeypot django-uni-form @@@ -42,6 -45,4 +45,6 @@@ pyenchan # OAI-PMH pyoai +egenix-mx-base +sunburnt