X-Git-Url: https://git.mdrn.pl/wolnelektury.git/blobdiff_plain/f906193db2ec44590f57beae2e8ee2df76117b7b..e7ef95e5867bba96cb445fef0d8932ab5ed6c938:/apps/catalogue/models.py?ds=inline diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py index 2b33964c2..670b66735 100644 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models.py @@ -3,7 +3,6 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from collections import namedtuple -from datetime import datetime from django.db import models from django.db.models import permalink, Q @@ -24,13 +23,17 @@ from django.conf import settings from newtagging.models import TagBase, tags_updated from newtagging import managers from catalogue.fields import JSONField, OverwritingFileField -from catalogue.utils import create_zip +from catalogue.utils import create_zip, split_tags +from catalogue.tasks import touch_tag, index_book from shutil import copy from glob import glob import re from os import path +import search + +# Those are hard-coded here so that makemessages sees them. TAG_CATEGORIES = ( ('author', _('author')), ('epoch', _('epoch')), @@ -61,7 +64,6 @@ class Tag(TagBase): category = models.CharField(_('category'), max_length=50, blank=False, null=False, db_index=True, choices=TAG_CATEGORIES) description = models.TextField(_('description'), blank=True) - main_page = models.BooleanField(_('main page'), default=False, db_index=True, help_text=_('Show tag on main page')) user = models.ForeignKey(User, blank=True, null=True) book_count = models.IntegerField(_('book count'), blank=True, null=True) @@ -106,25 +108,22 @@ class Tag(TagBase): has_description.boolean = True def get_count(self): - """ returns global book count for book tags, fragment count for themes """ - - if self.book_count is None: - if self.category == 'book': - # never used - objects = Book.objects.none() - elif self.category == 'theme': - objects = Fragment.tagged.with_all((self,)) - else: - objects = Book.tagged.with_all((self,)).order_by() - if self.category != 'set': - # eliminate descendants - l_tags = Tag.objects.filter(slug__in=[book.book_tag_slug() for book in objects]) - descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)] - if descendants_keys: - objects = objects.exclude(pk__in=descendants_keys) - self.book_count = objects.count() - self.save() - return self.book_count + """Returns global book count for book tags, fragment count for themes.""" + + if self.category == 'book': + # never used + objects = Book.objects.none() + elif self.category == 'theme': + objects = Fragment.tagged.with_all((self,)) + else: + objects = Book.tagged.with_all((self,)).order_by() + if self.category != 'set': + # eliminate descendants + l_tags = Tag.objects.filter(slug__in=[book.book_tag_slug() for book in objects]) + descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)] + if descendants_keys: + objects = objects.exclude(pk__in=descendants_keys) + return objects.count() @staticmethod def get_tag_list(tags): @@ -178,12 +177,16 @@ class Tag(TagBase): try: tag_names = getattr(info, field_name) except: - tag_names = [getattr(info, category)] + try: + tag_names = [getattr(info, category)] + except: + # For instance, Pictures do not have 'genre' field. + continue for tag_name in tag_names: tag_sort_key = tag_name if category == 'author': tag_sort_key = tag_name.last_name - tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name + tag_name = tag_name.readable() tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category) if created: tag.name = tag_name @@ -220,7 +223,7 @@ def get_customized_pdf_path(book, customizations): customizations.sort() h = hash(tuple(customizations)) - pdf_name = '%s-custom-%s' % (book.fileid(), h) + pdf_name = '%s-custom-%s' % (book.slug, h) pdf_file = get_dynamic_path(None, pdf_name, ext='pdf') return pdf_file @@ -230,7 +233,7 @@ def get_existing_customized_pdf(book): """ Returns a list of paths to generated customized pdf of a book """ - pdf_glob = '%s-custom-' % (book.fileid(),) + pdf_glob = '%s-custom-' % (book.slug,) pdf_glob = get_dynamic_path(None, pdf_glob, ext='pdf') pdf_glob = re.sub(r"[.]([a-z0-9]+)$", "*.\\1", pdf_glob) return glob(path.join(settings.MEDIA_ROOT, pdf_glob)) @@ -269,7 +272,7 @@ class BookMedia(models.Model): try: old = BookMedia.objects.get(pk=self.pk) except BookMedia.DoesNotExist, e: - pass + old = None else: # if name changed, change the file name, too if slughifi(self.name) != slughifi(old.name): @@ -278,7 +281,9 @@ class BookMedia(models.Model): super(BookMedia, self).save(*args, **kwargs) # remove the zip package for book with modified media - remove_zip(self.book.fileid()) + if old: + remove_zip("%s_%s" % (old.book.slug, old.type)) + remove_zip("%s_%s" % (self.book.slug, self.type)) extra_info = self.get_extra_info_value() extra_info.update(self.read_meta()) @@ -347,7 +352,9 @@ class BookMedia(models.Model): 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, db_index=True) + slug = models.SlugField(_('slug'), max_length=120, 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) description = models.TextField(_('description'), blank=True) @@ -359,6 +366,8 @@ class Book(models.Model): 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', 'txt'] formats = ebook_formats + ['html', 'xml'] @@ -370,14 +379,10 @@ class Book(models.Model): html_built = django.dispatch.Signal() published = django.dispatch.Signal() - URLID_RE = r'[a-z0-9-]+(?:/[a-z]{3})?' - FILEID_RE = r'[a-z0-9-]+(?:_[a-z]{3})?' - class AlreadyExists(Exception): pass class Meta: - unique_together = [['slug', 'language']] ordering = ('sort_key',) verbose_name = _('book') verbose_name_plural = _('books') @@ -385,42 +390,6 @@ class Book(models.Model): def __unicode__(self): return self.title - def urlid(self, sep='/'): - stem = self.slug - if self.language != settings.CATALOGUE_DEFAULT_LANGUAGE: - stem += sep + self.language - return stem - - def fileid(self): - return self.urlid('_') - - @staticmethod - def split_urlid(urlid, sep='/', default_lang=settings.CATALOGUE_DEFAULT_LANGUAGE): - """Splits a URL book id into slug and language code. - - Returns a dictionary usable i.e. for object lookup, or None. - - >>> Book.split_urlid("a-slug/pol", default_lang="eng") - {'slug': 'a-slug', 'language': 'pol'} - >>> Book.split_urlid("a-slug", default_lang="eng") - {'slug': 'a-slug', 'language': 'eng'} - >>> Book.split_urlid("a-slug_pol", "_", default_lang="eng") - {'slug': 'a-slug', 'language': 'pol'} - >>> Book.split_urlid("a-slug/eng", default_lang="eng") - - """ - parts = urlid.rsplit(sep, 1) - if len(parts) == 2: - if parts[1] == default_lang: - return None - return {'slug': parts[0], 'language': parts[1]} - else: - return {'slug': urlid, 'language': default_lang} - - @classmethod - def split_fileid(cls, fileid): - return cls.split_urlid(fileid, '_') - def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs): from sortify import sortify @@ -435,18 +404,14 @@ class Book(models.Model): @permalink def get_absolute_url(self): - return ('catalogue.views.book_detail', [self.urlid()]) + return ('catalogue.views.book_detail', [self.slug]) @property def name(self): return self.title def book_tag_slug(self): - stem = 'l-' + self.slug - if self.language != settings.CATALOGUE_DEFAULT_LANGUAGE: - return stem[:116] + ' ' + self.language - else: - return stem[:120] + return ('l-' + self.slug)[:120] def book_tag(self): slug = self.book_tag_slug() @@ -488,6 +453,7 @@ class Book(models.Model): cache_key = "Book.short_html/%d/%s" for lang, langname in settings.LANGUAGES: cache.delete(cache_key % (self.id, lang)) + cache.delete("Book.mini_box/%d" % (self.id, )) # Fragment.short_html relies on book's tags, so reset it here too for fragm in self.fragments.all(): fragm.reset_short_html() @@ -502,24 +468,15 @@ class Book(models.Model): if short_html is not None: return mark_safe(short_html) else: - tags = self.tags.filter(~Q(category__in=('set', 'theme', 'book'))) - tags = [mark_safe(u'%s' % (tag.get_absolute_url(), tag.name)) for tag in tags] + tags = self.tags.filter(category__in=('author', 'kind', 'genre', 'epoch')) + tags = split_tags(tags) - formats = [] + formats = {} # files generated during publication - if self.has_media("html"): - formats.append(u'%s' % (reverse('book_text', args=[self.fileid()]), _('Read online'))) for ebook_format in self.ebook_formats: if self.has_media(ebook_format): - formats.append(u'%s' % ( - self.get_media(ebook_format).url, - ebook_format.upper() - )) - # other files - for m in self.media.order_by('type'): - formats.append(u'%s' % (m.file.url, m.type.upper())) + formats[ebook_format] = self.get_media(ebook_format) - formats = [mark_safe(format) for format in formats] short_html = unicode(render_to_string('catalogue/book_short.html', {'book': self, 'tags': tags, 'formats': formats})) @@ -530,7 +487,7 @@ class Book(models.Model): def mini_box(self): if self.id: - cache_key = "Book.mini_boxs/%d" % (self.id, ) + cache_key = "Book.mini_box/%d" % (self.id, ) short_html = cache.get(cache_key) else: short_html = None @@ -567,13 +524,27 @@ class Book(models.Model): has_daisy_file.boolean = True def wldocument(self, parse_dublincore=True): - from catalogue.utils import ORMDocProvider + from catalogue.import_utils import ORMDocProvider from librarian.parser import WLDocument 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_pdf(self, customizations=None, file_name=None): """ (Re)builds the pdf file. customizations - customizations which are passed to LaTeX class file. @@ -589,7 +560,7 @@ class Book(models.Model): # we'd like to be sure not to overwrite changes happening while # (timely) pdf generation is taking place (async celery scenario) current_self = Book.objects.get(id=self.id) - current_self.pdf_file.save('%s.pdf' % self.fileid(), + current_self.pdf_file.save('%s.pdf' % self.slug, File(open(pdf.get_filename()))) self.pdf_file = current_self.pdf_file @@ -612,7 +583,7 @@ class Book(models.Model): mobi = self.wldocument().as_mobi() - self.mobi_file.save('%s.mobi' % self.fileid(), File(open(mobi.get_filename()))) + self.mobi_file.save('%s.mobi' % self.slug, File(open(mobi.get_filename()))) # remove zip with all mobi files remove_zip(settings.ALL_MOBI_ZIP) @@ -624,7 +595,7 @@ class Book(models.Model): epub = self.wldocument().as_epub() - self.epub_file.save('%s.epub' % self.fileid(), + self.epub_file.save('%s.epub' % self.slug, File(open(epub.get_filename()))) # remove zip package with all epub files @@ -634,7 +605,7 @@ class Book(models.Model): from django.core.files.base import ContentFile text = self.wldocument().as_text() - self.txt_file.save('%s.txt' % self.fileid(), ContentFile(text.get_string())) + self.txt_file.save('%s.txt' % self.slug, ContentFile(text.get_string())) def build_html(self): @@ -649,7 +620,7 @@ class Book(models.Model): html_output = self.wldocument(parse_dublincore=False).as_html() if html_output: - self.html_file.save('%s.html' % self.fileid(), + self.html_file.save('%s.html' % self.slug, ContentFile(html_output.get_string())) # get ancestor l-tags for adding to new fragments @@ -700,7 +671,7 @@ class Book(models.Model): def pretty_file_name(book): return "%s/%s.%s" % ( b.get_extra_info_value()['author'], - b.fileid(), + b.slug, format_) field_name = "%s_file" % format_ @@ -711,12 +682,25 @@ class Book(models.Model): getattr(settings, "ALL_%s_ZIP" % format_.upper())) return result.wait() - def zip_audiobooks(self): - bm = BookMedia.objects.filter(book=self, type='mp3') + def zip_audiobooks(self, format_): + bm = BookMedia.objects.filter(book=self, type=format_) paths = map(lambda bm: (None, bm.file.path), bm) - result = create_zip.delay(paths, self.fileid()) + result = create_zip.delay(paths, "%s_%s" % (self.slug, format_)) return result.wait() + def search_index(self, book_info=None): + if settings.CELERY_ALWAYS_EAGER: + idx = search.ReusableIndex() + else: + idx = search.Index() + + idx.open() + try: + idx.index_book(self, book_info) + idx.index_tags() + finally: + idx.close() + @classmethod def from_xml_file(cls, xml_file, **kwargs): from django.core.files import File @@ -735,7 +719,8 @@ class Book(models.Model): @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_epub=True, build_txt=True, build_pdf=True, build_mobi=True, + search_index=True): import re from sortify import sortify @@ -744,30 +729,33 @@ class Book(models.Model): if hasattr(book_info, 'parts'): for part_url in book_info.parts: try: - children.append(Book.objects.get( - slug=part_url.slug, language=part_url.language)) + children.append(Book.objects.get(slug=part_url.slug)) except Book.DoesNotExist, e: - raise Book.DoesNotExist(_('Book "%s/%s" does not exist.') % - (part_url.slug, part_url.language)) + raise Book.DoesNotExist(_('Book "%s" does not exist.') % + part_url.slug) # Read book metadata book_slug = book_info.url.slug - language = book_info.language - if re.search(r'[^a-zA-Z0-9-]', book_slug): + if re.search(r'[^a-z0-9-]', book_slug): raise ValueError('Invalid characters in slug') - book, created = Book.objects.get_or_create(slug=book_slug, language=language) + book, created = Book.objects.get_or_create(slug=book_slug) if created: book_shelves = [] else: if not overwrite: - raise Book.AlreadyExists(_('Book %s/%s already exists') % ( - book_slug, language)) + raise Book.AlreadyExists(_('Book %s already exists') % ( + book_slug)) # Save shelves for this book book_shelves = list(book.tags.filter(category='set')) + book.language = book_info.language book.title = book_info.title + if book_info.variant_of: + book.common_slug = book_info.variant_of.slug + else: + book.common_slug = book.slug book.set_extra_info_value(book_info.to_dict()) book.save() @@ -792,6 +780,8 @@ class Book(models.Model): if not settings.NO_BUILD_TXT and build_txt: book.build_txt() + book.build_cover(book_info) + if not settings.NO_BUILD_EPUB and build_epub: book.build_epub() @@ -801,16 +791,24 @@ class Book(models.Model): if not settings.NO_BUILD_MOBI and build_mobi: book.build_mobi() + if not settings.NO_SEARCH_INDEX and search_index: + index_book.delay(book.id, book_info) + book_descendants = list(book.children.all()) + descendants_tags = set() # add l-tag to descendants and their fragments while len(book_descendants) > 0: child_book = book_descendants.pop(0) + descendants_tags.update(child_book.tags) child_book.tags = list(child_book.tags) + [book_tag] child_book.save() for fragment in child_book.fragments.all(): fragment.tags = set(list(fragment.tags) + [book_tag]) book_descendants += list(child_book.children.all()) + for tag in descendants_tags: + touch_tag.delay(tag) + book.save() # refresh cache @@ -921,7 +919,7 @@ class Book(models.Model): books_by_parent = {} books = cls.objects.all().order_by('parent_number', 'sort_key').only( - 'title', 'parent', 'slug', 'language') + 'title', 'parent', 'slug') if filter: books = books.filter(filter).distinct() book_ids = set((book.pk for book in books)) @@ -997,7 +995,7 @@ class Fragment(models.Model): verbose_name_plural = _('fragments') def get_absolute_url(self): - return '%s#m%s' % (self.book.get_html_url(), self.anchor) + return '%s#m%s' % (reverse('book_text', args=[self.book.slug]), self.anchor) def reset_short_html(self): if self.id is None: @@ -1034,7 +1032,8 @@ class Fragment(models.Model): def _tags_updated_handler(sender, affected_tags, **kwargs): # reset tag global counter # 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()) + for tag in affected_tags: + touch_tag.delay(tag) # if book tags changed, reset book tag counter if isinstance(sender, Book) and \