From c661073dd0969caa9a47a225f3d3fae767295c3d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 9 Aug 2012 17:09:06 +0200 Subject: [PATCH] split catalogue.models --- apps/catalogue/constants.py | 0 apps/catalogue/import_utils.py | 0 apps/catalogue/models/__init__.py | 10 + apps/catalogue/{models.py => models/book.py} | 442 +------------------ apps/catalogue/models/bookmedia.py | 123 ++++++ apps/catalogue/models/collection.py | 25 ++ apps/catalogue/models/fragment.py | 65 +++ apps/catalogue/models/listeners.py | 58 +++ apps/catalogue/models/tag.py | 166 +++++++ apps/catalogue/tasks.py | 0 apps/catalogue/tests/book_import.py | 2 +- apps/catalogue/utils.py | 19 + apps/newtagging/models.py | 1 + requirements-dev.txt | 0 14 files changed, 476 insertions(+), 435 deletions(-) mode change 100755 => 100644 apps/catalogue/constants.py mode change 100755 => 100644 apps/catalogue/import_utils.py create mode 100644 apps/catalogue/models/__init__.py rename apps/catalogue/{models.py => models/book.py} (57%) create mode 100644 apps/catalogue/models/bookmedia.py create mode 100644 apps/catalogue/models/collection.py create mode 100644 apps/catalogue/models/fragment.py create mode 100644 apps/catalogue/models/listeners.py create mode 100644 apps/catalogue/models/tag.py mode change 100755 => 100644 apps/catalogue/tasks.py mode change 100755 => 100644 requirements-dev.txt diff --git a/apps/catalogue/constants.py b/apps/catalogue/constants.py old mode 100755 new mode 100644 diff --git a/apps/catalogue/import_utils.py b/apps/catalogue/import_utils.py old mode 100755 new mode 100644 diff --git a/apps/catalogue/models/__init__.py b/apps/catalogue/models/__init__.py new file mode 100644 index 000000000..541ab5c8c --- /dev/null +++ b/apps/catalogue/models/__init__.py @@ -0,0 +1,10 @@ +# -*- 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 catalogue.models.tag import Tag +from catalogue.models.bookmedia import BookMedia +from catalogue.models.fragment import Fragment +from catalogue.models.book import Book +from catalogue.models.collection import Collection +from catalogue.models.listeners import * diff --git a/apps/catalogue/models.py b/apps/catalogue/models/book.py similarity index 57% rename from apps/catalogue/models.py rename to apps/catalogue/models/book.py index 3704b1663..4b50fc9fc 100644 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models/book.py @@ -2,330 +2,24 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from collections import namedtuple - +import re +from django.conf import settings +from django.core.cache import get_cache from django.db import models from django.db.models import permalink import django.dispatch -from django.core.cache import get_cache -from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import User -from django.template.loader import render_to_string from django.utils.datastructures import SortedDict -from django.utils.safestring import mark_safe -from django.utils.translation import get_language -from django.core.urlresolvers import reverse -from django.db.models.signals import post_save, pre_delete, post_delete +from django.utils.translation import ugettext_lazy as _ import jsonfield - -from django.conf import settings - -from newtagging.models import TagBase, tags_updated -from newtagging import managers -from catalogue.fields import OverwritingFileField -from catalogue.utils import create_zip, split_tags, truncate_html_words +from catalogue.models import Tag, Fragment, BookMedia +from catalogue.utils import create_zip, split_tags, truncate_html_words, book_upload_path from catalogue import tasks -import re - - -# Those are hard-coded here so that makemessages sees them. -TAG_CATEGORIES = ( - ('author', _('author')), - ('epoch', _('epoch')), - ('kind', _('kind')), - ('genre', _('genre')), - ('theme', _('theme')), - ('set', _('set')), - ('book', _('book')), -) +from newtagging import managers permanent_cache = get_cache('permanent') -class TagSubcategoryManager(models.Manager): - def __init__(self, subcategory): - super(TagSubcategoryManager, self).__init__() - self.subcategory = subcategory - - def get_query_set(self): - return super(TagSubcategoryManager, self).get_query_set().filter(category=self.subcategory) - - -class Tag(TagBase): - """A tag attachable to books and fragments (and possibly anything). - - Used to represent searchable metadata (authors, epochs, genres, kinds), - fragment themes (motifs) and some book hierarchy related kludges.""" - name = models.CharField(_('name'), max_length=50, db_index=True) - slug = models.SlugField(_('slug'), max_length=120, db_index=True) - sort_key = models.CharField(_('sort key'), max_length=120, db_index=True) - category = models.CharField(_('category'), max_length=50, blank=False, null=False, - db_index=True, choices=TAG_CATEGORIES) - description = models.TextField(_('description'), blank=True) - - user = models.ForeignKey(User, blank=True, null=True) - book_count = models.IntegerField(_('book count'), blank=True, null=True) - gazeta_link = models.CharField(blank=True, max_length=240) - wiki_link = models.CharField(blank=True, max_length=240) - - created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True) - changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True) - - class UrlDeprecationWarning(DeprecationWarning): - pass - - categories_rev = { - 'autor': 'author', - 'epoka': 'epoch', - 'rodzaj': 'kind', - 'gatunek': 'genre', - 'motyw': 'theme', - 'polka': 'set', - } - categories_dict = dict((item[::-1] for item in categories_rev.iteritems())) - - class Meta: - ordering = ('sort_key',) - verbose_name = _('tag') - verbose_name_plural = _('tags') - unique_together = (("slug", "category"),) - - def __unicode__(self): - return self.name - - def __repr__(self): - return "Tag(slug=%r)" % self.slug - - @permalink - def get_absolute_url(self): - return ('catalogue.views.tagged_object_list', [self.url_chunk]) - - def has_description(self): - return len(self.description) > 0 - has_description.short_description = _('description') - has_description.boolean = True - - def get_count(self): - """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.iterator()]) - descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags).iterator()] - if descendants_keys: - objects = objects.exclude(pk__in=descendants_keys) - return objects.count() - - @staticmethod - def get_tag_list(tags): - if isinstance(tags, basestring): - real_tags = [] - ambiguous_slugs = [] - category = None - deprecated = False - tags_splitted = tags.split('/') - for name in tags_splitted: - if category: - real_tags.append(Tag.objects.get(slug=name, category=category)) - category = None - elif name in Tag.categories_rev: - category = Tag.categories_rev[name] - else: - try: - real_tags.append(Tag.objects.exclude(category='book').get(slug=name)) - deprecated = True - except Tag.MultipleObjectsReturned, e: - ambiguous_slugs.append(name) - - if category: - # something strange left off - raise Tag.DoesNotExist() - if ambiguous_slugs: - # some tags should be qualified - e = Tag.MultipleObjectsReturned() - e.tags = real_tags - e.ambiguous_slugs = ambiguous_slugs - raise e - if deprecated: - e = Tag.UrlDeprecationWarning() - e.tags = real_tags - raise e - return real_tags - else: - return TagBase.get_tag_list(tags) - - @property - def url_chunk(self): - return '/'.join((Tag.categories_dict[self.category], self.slug)) - - @staticmethod - def tags_from_info(info): - from slughifi import slughifi - from sortify import sortify - meta_tags = [] - categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch')) - for field_name, category in categories: - try: - tag_names = getattr(info, field_name) - except: - 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 = tag_name.readable() - tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category) - if created: - tag.name = tag_name - tag.sort_key = sortify(tag_sort_key.lower()) - tag.save() - meta_tags.append(tag) - return meta_tags - - - -def get_dynamic_path(media, filename, ext=None, maxlen=100): - from slughifi import slughifi - - # how to put related book's slug here? - if not ext: - # BookMedia case - ext = media.formats[media.type].ext - if media is None or not media.name: - name = slughifi(filename.split(".")[0]) - else: - name = slughifi(media.name) - return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext) - - -# TODO: why is this hard-coded ? -def book_upload_path(ext=None, maxlen=100): - return lambda *args: get_dynamic_path(*args, ext=ext, maxlen=maxlen) - - -class BookMedia(models.Model): - """Represents media attached to a book.""" - FileFormat = namedtuple("FileFormat", "name ext") - formats = SortedDict([ - ('mp3', FileFormat(name='MP3', ext='mp3')), - ('ogg', FileFormat(name='Ogg Vorbis', ext='ogg')), - ('daisy', FileFormat(name='DAISY', ext='daisy.zip')), - ]) - format_choices = [(k, _('%s file') % t.name) - for k, t in formats.items()] - - type = models.CharField(_('type'), choices=format_choices, max_length="100") - name = models.CharField(_('name'), max_length="100") - file = OverwritingFileField(_('file'), upload_to=book_upload_path()) - uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False) - extra_info = jsonfield.JSONField(_('extra information'), default='{}', editable=False) - book = models.ForeignKey('Book', related_name='media') - source_sha1 = models.CharField(null=True, blank=True, max_length=40, editable=False) - - def __unicode__(self): - return "%s (%s)" % (self.name, self.file.name.split("/")[-1]) - - class Meta: - ordering = ('type', 'name') - verbose_name = _('book media') - verbose_name_plural = _('book media') - - def save(self, *args, **kwargs): - from slughifi import slughifi - from catalogue.utils import ExistingFile, remove_zip - - try: - old = BookMedia.objects.get(pk=self.pk) - except BookMedia.DoesNotExist: - old = None - else: - # if name changed, change the file name, too - if slughifi(self.name) != slughifi(old.name): - self.file.save(None, ExistingFile(self.file.path), save=False, leave=True) - - super(BookMedia, self).save(*args, **kwargs) - - # remove the zip package for book with modified media - if old: - remove_zip("%s_%s" % (old.book.slug, old.type)) - remove_zip("%s_%s" % (self.book.slug, self.type)) - - extra_info = self.extra_info - extra_info.update(self.read_meta()) - self.extra_info = extra_info - self.source_sha1 = self.read_source_sha1(self.file.path, self.type) - return super(BookMedia, self).save(*args, **kwargs) - - def read_meta(self): - """ - Reads some metadata from the audiobook. - """ - import mutagen - from mutagen import id3 - - artist_name = director_name = project = funded_by = '' - if self.type == 'mp3': - try: - audio = id3.ID3(self.file.path) - artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1')) - director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3')) - project = ", ".join([t.data for t in audio.getall('PRIV') - if t.owner=='wolnelektury.pl?project']) - funded_by = ", ".join([t.data for t in audio.getall('PRIV') - if t.owner=='wolnelektury.pl?funded_by']) - except: - pass - elif self.type == 'ogg': - try: - audio = mutagen.File(self.file.path) - artist_name = ', '.join(audio.get('artist', [])) - director_name = ', '.join(audio.get('conductor', [])) - project = ", ".join(audio.get('project', [])) - funded_by = ", ".join(audio.get('funded_by', [])) - except: - pass - else: - return {} - return {'artist_name': artist_name, 'director_name': director_name, - 'project': project, 'funded_by': funded_by} - - @staticmethod - def read_source_sha1(filepath, filetype): - """ - Reads source file SHA1 from audiobok metadata. - """ - import mutagen - from mutagen import id3 - - if filetype == 'mp3': - try: - audio = id3.ID3(filepath) - return [t.data for t in audio.getall('PRIV') - if t.owner=='wolnelektury.pl?flac_sha1'][0] - except: - return None - elif filetype == 'ogg': - try: - audio = mutagen.File(filepath) - return audio.get('flac_sha1', [None])[0] - except: - return None - else: - return None - - class Book(models.Model): """Represents a book imported from WL-XML.""" title = models.CharField(_('title'), max_length=120) @@ -367,6 +61,7 @@ class Book(models.Model): ordering = ('sort_key',) verbose_name = _('book') verbose_name_plural = _('books') + app_label = 'catalogue' def __unicode__(self): return self.title @@ -917,124 +612,3 @@ for t in Book.formats: blank=True).contribute_to_class(Book, field_name) setattr(Book, "has_%s_file" % t, _has_factory(t)) - - -class Fragment(models.Model): - """Represents a themed fragment of a book.""" - text = models.TextField() - short_text = models.TextField(editable=False) - anchor = models.CharField(max_length=120) - book = models.ForeignKey(Book, related_name='fragments') - - objects = models.Manager() - tagged = managers.ModelTaggedItemManager(Tag) - tags = managers.TagDescriptor(Tag) - - class Meta: - ordering = ('book', 'anchor',) - verbose_name = _('fragment') - verbose_name_plural = _('fragments') - - def get_absolute_url(self): - return '%s#m%s' % (reverse('book_text', args=[self.book.slug]), self.anchor) - - def reset_short_html(self): - if self.id is None: - return - - cache_key = "Fragment.short_html/%d/%s" - for lang, langname in settings.LANGUAGES: - permanent_cache.delete(cache_key % (self.id, lang)) - - def get_short_text(self): - """Returns short version of the fragment.""" - return self.short_text if self.short_text else self.text - - def short_html(self): - if self.id: - cache_key = "Fragment.short_html/%d/%s" % (self.id, get_language()) - short_html = permanent_cache.get(cache_key) - else: - short_html = None - - if short_html is not None: - return mark_safe(short_html) - else: - short_html = unicode(render_to_string('catalogue/fragment_short.html', - {'fragment': self})) - if self.id: - permanent_cache.set(cache_key, short_html) - return mark_safe(short_html) - - -class Collection(models.Model): - """A collection of books, which might be defined before publishing them.""" - title = models.CharField(_('title'), max_length=120, db_index=True) - slug = models.SlugField(_('slug'), max_length=120, primary_key=True) - description = models.TextField(_('description'), null=True, blank=True) - - models.SlugField(_('slug'), max_length=120, unique=True, db_index=True) - book_slugs = models.TextField(_('book slugs')) - - class Meta: - ordering = ('title',) - verbose_name = _('collection') - verbose_name_plural = _('collections') - - def __unicode__(self): - return self.title - - -########### -# -# SIGNALS -# -########### - - -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 - for tag in affected_tags: - tasks.touch_tag(tag) - - # if book tags changed, reset book tag counter - if isinstance(sender, Book) and \ - Tag.objects.filter(pk__in=(tag.pk for tag in affected_tags)).\ - exclude(category__in=('book', 'theme', 'set')).count(): - sender.reset_tag_counter() - # if fragment theme changed, reset book theme counter - elif isinstance(sender, Fragment) and \ - Tag.objects.filter(pk__in=(tag.pk for tag in affected_tags)).\ - filter(category='theme').count(): - sender.book.reset_theme_counter() -tags_updated.connect(_tags_updated_handler) - - -def _pre_delete_handler(sender, instance, **kwargs): - """ refresh Book on BookMedia delete """ - if sender == BookMedia: - instance.book.save() -pre_delete.connect(_pre_delete_handler) - - -def _post_save_handler(sender, instance, **kwargs): - """ refresh all the short_html stuff on BookMedia update """ - if sender == BookMedia: - instance.book.save() -post_save.connect(_post_save_handler) - - -if not settings.NO_SEARCH_INDEX: - @django.dispatch.receiver(post_delete, sender=Book) - def _remove_book_from_index_handler(sender, instance, **kwargs): - """ remove the book from search index, when it is deleted.""" - import search - search.JVM.attachCurrentThread() - idx = search.Index() - idx.open(timeout=10000) # 10 seconds timeout. - try: - idx.remove_book(instance) - idx.index_tags() - finally: - idx.close() diff --git a/apps/catalogue/models/bookmedia.py b/apps/catalogue/models/bookmedia.py new file mode 100644 index 000000000..024f713c2 --- /dev/null +++ b/apps/catalogue/models/bookmedia.py @@ -0,0 +1,123 @@ +# -*- 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 collections import namedtuple +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.utils.datastructures import SortedDict +import jsonfield +from catalogue.fields import OverwritingFileField +from catalogue.utils import book_upload_path + + +class BookMedia(models.Model): + """Represents media attached to a book.""" + FileFormat = namedtuple("FileFormat", "name ext") + formats = SortedDict([ + ('mp3', FileFormat(name='MP3', ext='mp3')), + ('ogg', FileFormat(name='Ogg Vorbis', ext='ogg')), + ('daisy', FileFormat(name='DAISY', ext='daisy.zip')), + ]) + format_choices = [(k, _('%s file') % t.name) + for k, t in formats.items()] + + type = models.CharField(_('type'), choices=format_choices, max_length="100") + name = models.CharField(_('name'), max_length="100") + file = OverwritingFileField(_('file'), upload_to=book_upload_path()) + uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False) + extra_info = jsonfield.JSONField(_('extra information'), default='{}', editable=False) + book = models.ForeignKey('Book', related_name='media') + source_sha1 = models.CharField(null=True, blank=True, max_length=40, editable=False) + + def __unicode__(self): + return "%s (%s)" % (self.name, self.file.name.split("/")[-1]) + + class Meta: + ordering = ('type', 'name') + verbose_name = _('book media') + verbose_name_plural = _('book media') + app_label = 'catalogue' + + def save(self, *args, **kwargs): + from slughifi import slughifi + from catalogue.utils import ExistingFile, remove_zip + + try: + old = BookMedia.objects.get(pk=self.pk) + except BookMedia.DoesNotExist: + old = None + else: + # if name changed, change the file name, too + if slughifi(self.name) != slughifi(old.name): + self.file.save(None, ExistingFile(self.file.path), save=False, leave=True) + + super(BookMedia, self).save(*args, **kwargs) + + # remove the zip package for book with modified media + if old: + remove_zip("%s_%s" % (old.book.slug, old.type)) + remove_zip("%s_%s" % (self.book.slug, self.type)) + + extra_info = self.extra_info + extra_info.update(self.read_meta()) + self.extra_info = extra_info + self.source_sha1 = self.read_source_sha1(self.file.path, self.type) + return super(BookMedia, self).save(*args, **kwargs) + + def read_meta(self): + """ + Reads some metadata from the audiobook. + """ + import mutagen + from mutagen import id3 + + artist_name = director_name = project = funded_by = '' + if self.type == 'mp3': + try: + audio = id3.ID3(self.file.path) + artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1')) + director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3')) + project = ", ".join([t.data for t in audio.getall('PRIV') + if t.owner=='wolnelektury.pl?project']) + funded_by = ", ".join([t.data for t in audio.getall('PRIV') + if t.owner=='wolnelektury.pl?funded_by']) + except: + pass + elif self.type == 'ogg': + try: + audio = mutagen.File(self.file.path) + artist_name = ', '.join(audio.get('artist', [])) + director_name = ', '.join(audio.get('conductor', [])) + project = ", ".join(audio.get('project', [])) + funded_by = ", ".join(audio.get('funded_by', [])) + except: + pass + else: + return {} + return {'artist_name': artist_name, 'director_name': director_name, + 'project': project, 'funded_by': funded_by} + + @staticmethod + def read_source_sha1(filepath, filetype): + """ + Reads source file SHA1 from audiobok metadata. + """ + import mutagen + from mutagen import id3 + + if filetype == 'mp3': + try: + audio = id3.ID3(filepath) + return [t.data for t in audio.getall('PRIV') + if t.owner=='wolnelektury.pl?flac_sha1'][0] + except: + return None + elif filetype == 'ogg': + try: + audio = mutagen.File(filepath) + return audio.get('flac_sha1', [None])[0] + except: + return None + else: + return None diff --git a/apps/catalogue/models/collection.py b/apps/catalogue/models/collection.py new file mode 100644 index 000000000..1c9027c31 --- /dev/null +++ b/apps/catalogue/models/collection.py @@ -0,0 +1,25 @@ +# -*- 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.db import models +from django.utils.translation import ugettext_lazy as _ + + +class Collection(models.Model): + """A collection of books, which might be defined before publishing them.""" + title = models.CharField(_('title'), max_length=120, db_index=True) + slug = models.SlugField(_('slug'), max_length=120, primary_key=True) + description = models.TextField(_('description'), null=True, blank=True) + + models.SlugField(_('slug'), max_length=120, unique=True, db_index=True) + book_slugs = models.TextField(_('book slugs')) + + class Meta: + ordering = ('title',) + verbose_name = _('collection') + verbose_name_plural = _('collections') + app_label = 'catalogue' + + def __unicode__(self): + return self.title diff --git a/apps/catalogue/models/fragment.py b/apps/catalogue/models/fragment.py new file mode 100644 index 000000000..c9131aa49 --- /dev/null +++ b/apps/catalogue/models/fragment.py @@ -0,0 +1,65 @@ +# -*- 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.conf import settings +from django.core.cache import get_cache +from django.core.urlresolvers import reverse +from django.db import models +from django.template.loader import render_to_string +from django.utils.safestring import mark_safe +from django.utils.translation import get_language, ugettext_lazy as _ +from newtagging import managers +from catalogue.models import Tag + + +permanent_cache = get_cache('permanent') + + +class Fragment(models.Model): + """Represents a themed fragment of a book.""" + text = models.TextField() + short_text = models.TextField(editable=False) + anchor = models.CharField(max_length=120) + book = models.ForeignKey('Book', related_name='fragments') + + objects = models.Manager() + tagged = managers.ModelTaggedItemManager(Tag) + tags = managers.TagDescriptor(Tag) + + class Meta: + ordering = ('book', 'anchor',) + verbose_name = _('fragment') + verbose_name_plural = _('fragments') + app_label = 'catalogue' + + def get_absolute_url(self): + return '%s#m%s' % (reverse('book_text', args=[self.book.slug]), self.anchor) + + def reset_short_html(self): + if self.id is None: + return + + cache_key = "Fragment.short_html/%d/%s" + for lang, langname in settings.LANGUAGES: + permanent_cache.delete(cache_key % (self.id, lang)) + + def get_short_text(self): + """Returns short version of the fragment.""" + return self.short_text if self.short_text else self.text + + def short_html(self): + if self.id: + cache_key = "Fragment.short_html/%d/%s" % (self.id, get_language()) + short_html = permanent_cache.get(cache_key) + else: + short_html = None + + if short_html is not None: + return mark_safe(short_html) + else: + short_html = unicode(render_to_string('catalogue/fragment_short.html', + {'fragment': self})) + if self.id: + permanent_cache.set(cache_key, short_html) + return mark_safe(short_html) diff --git a/apps/catalogue/models/listeners.py b/apps/catalogue/models/listeners.py new file mode 100644 index 000000000..9e9fcabff --- /dev/null +++ b/apps/catalogue/models/listeners.py @@ -0,0 +1,58 @@ +# -*- 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.conf import settings +from django.db.models.signals import post_save, pre_delete, post_delete +import django.dispatch +from catalogue.models import Tag, BookMedia, Book, Fragment +from catalogue import tasks +from newtagging.models import tags_updated + + +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 + for tag in affected_tags: + tasks.touch_tag(tag) + + # if book tags changed, reset book tag counter + if isinstance(sender, Book) and \ + Tag.objects.filter(pk__in=(tag.pk for tag in affected_tags)).\ + exclude(category__in=('book', 'theme', 'set')).count(): + sender.reset_tag_counter() + # if fragment theme changed, reset book theme counter + elif isinstance(sender, Fragment) and \ + Tag.objects.filter(pk__in=(tag.pk for tag in affected_tags)).\ + filter(category='theme').count(): + sender.book.reset_theme_counter() +tags_updated.connect(_tags_updated_handler) + + +def _pre_delete_handler(sender, instance, **kwargs): + """ refresh Book on BookMedia delete """ + if sender == BookMedia: + instance.book.save() +pre_delete.connect(_pre_delete_handler) + + +def _post_save_handler(sender, instance, **kwargs): + """ refresh all the short_html stuff on BookMedia update """ + if sender == BookMedia: + instance.book.save() +post_save.connect(_post_save_handler) + + +if not settings.NO_SEARCH_INDEX: + @django.dispatch.receiver(post_delete, sender=Book) + def _remove_book_from_index_handler(sender, instance, **kwargs): + """ remove the book from search index, when it is deleted.""" + import search + search.JVM.attachCurrentThread() + idx = search.Index() + idx.open(timeout=10000) # 10 seconds timeout. + try: + idx.remove_book(instance) + idx.index_tags() + finally: + idx.close() diff --git a/apps/catalogue/models/tag.py b/apps/catalogue/models/tag.py new file mode 100644 index 000000000..353d567bb --- /dev/null +++ b/apps/catalogue/models/tag.py @@ -0,0 +1,166 @@ +# -*- 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.contrib.auth.models import User +from django.db import models +from django.db.models import permalink +from django.utils.translation import ugettext_lazy as _ +from newtagging.models import TagBase + + +# Those are hard-coded here so that makemessages sees them. +TAG_CATEGORIES = ( + ('author', _('author')), + ('epoch', _('epoch')), + ('kind', _('kind')), + ('genre', _('genre')), + ('theme', _('theme')), + ('set', _('set')), + ('book', _('book')), +) + + +class Tag(TagBase): + """A tag attachable to books and fragments (and possibly anything). + + Used to represent searchable metadata (authors, epochs, genres, kinds), + fragment themes (motifs) and some book hierarchy related kludges.""" + name = models.CharField(_('name'), max_length=50, db_index=True) + slug = models.SlugField(_('slug'), max_length=120, db_index=True) + sort_key = models.CharField(_('sort key'), max_length=120, db_index=True) + category = models.CharField(_('category'), max_length=50, blank=False, null=False, + db_index=True, choices=TAG_CATEGORIES) + description = models.TextField(_('description'), blank=True) + + user = models.ForeignKey(User, blank=True, null=True) + book_count = models.IntegerField(_('book count'), blank=True, null=True) + gazeta_link = models.CharField(blank=True, max_length=240) + wiki_link = models.CharField(blank=True, max_length=240) + + created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True) + changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True) + + class UrlDeprecationWarning(DeprecationWarning): + pass + + categories_rev = { + 'autor': 'author', + 'epoka': 'epoch', + 'rodzaj': 'kind', + 'gatunek': 'genre', + 'motyw': 'theme', + 'polka': 'set', + } + categories_dict = dict((item[::-1] for item in categories_rev.iteritems())) + + class Meta: + ordering = ('sort_key',) + verbose_name = _('tag') + verbose_name_plural = _('tags') + unique_together = (("slug", "category"),) + app_label = 'catalogue' + + def __unicode__(self): + return self.name + + def __repr__(self): + return "Tag(slug=%r)" % self.slug + + @permalink + def get_absolute_url(self): + return ('catalogue.views.tagged_object_list', [self.url_chunk]) + + def has_description(self): + return len(self.description) > 0 + has_description.short_description = _('description') + has_description.boolean = True + + def get_count(self): + """Returns global book count for book tags, fragment count for themes.""" + from catalogue.models import Book, Fragment + + 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.iterator()]) + descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags).iterator()] + if descendants_keys: + objects = objects.exclude(pk__in=descendants_keys) + return objects.count() + + @staticmethod + def get_tag_list(tags): + if isinstance(tags, basestring): + real_tags = [] + ambiguous_slugs = [] + category = None + deprecated = False + tags_splitted = tags.split('/') + for name in tags_splitted: + if category: + real_tags.append(Tag.objects.get(slug=name, category=category)) + category = None + elif name in Tag.categories_rev: + category = Tag.categories_rev[name] + else: + try: + real_tags.append(Tag.objects.exclude(category='book').get(slug=name)) + deprecated = True + except Tag.MultipleObjectsReturned, e: + ambiguous_slugs.append(name) + + if category: + # something strange left off + raise Tag.DoesNotExist() + if ambiguous_slugs: + # some tags should be qualified + e = Tag.MultipleObjectsReturned() + e.tags = real_tags + e.ambiguous_slugs = ambiguous_slugs + raise e + if deprecated: + e = Tag.UrlDeprecationWarning() + e.tags = real_tags + raise e + return real_tags + else: + return TagBase.get_tag_list(tags) + + @property + def url_chunk(self): + return '/'.join((Tag.categories_dict[self.category], self.slug)) + + @staticmethod + def tags_from_info(info): + from slughifi import slughifi + from sortify import sortify + meta_tags = [] + categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch')) + for field_name, category in categories: + try: + tag_names = getattr(info, field_name) + except: + 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 = tag_name.readable() + tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category) + if created: + tag.name = tag_name + tag.sort_key = sortify(tag_sort_key.lower()) + tag.save() + meta_tags.append(tag) + return meta_tags diff --git a/apps/catalogue/tasks.py b/apps/catalogue/tasks.py old mode 100755 new mode 100644 diff --git a/apps/catalogue/tests/book_import.py b/apps/catalogue/tests/book_import.py index 6ece3287e..904c1bf1d 100644 --- a/apps/catalogue/tests/book_import.py +++ b/apps/catalogue/tests/book_import.py @@ -297,7 +297,7 @@ class BookImportGenerateTest(WLTestCase): self.assertTrue(path.exists(parent.pdf_file.path)) def test_custom_pdf(self): - from catalogue.tasks import build_custom_pdf + from catalogue.utils import build_custom_pdf out = models.get_dynamic_path(None, 'test-custom', ext='pdf') absoulute_path = path.join(settings.MEDIA_ROOT, out) diff --git a/apps/catalogue/utils.py b/apps/catalogue/utils.py index 29f40d16b..bda7e0475 100644 --- a/apps/catalogue/utils.py +++ b/apps/catalogue/utils.py @@ -44,6 +44,25 @@ def split_tags(tags): return result +def get_dynamic_path(media, filename, ext=None, maxlen=100): + from slughifi import slughifi + + # how to put related book's slug here? + if not ext: + # BookMedia case + ext = media.formats[media.type].ext + if media is None or not media.name: + name = slughifi(filename.split(".")[0]) + else: + name = slughifi(media.name) + return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext) + + +# TODO: why is this hard-coded ? +def book_upload_path(ext=None, maxlen=100): + return lambda *args: get_dynamic_path(*args, ext=ext, maxlen=maxlen) + + class ExistingFile(UploadedFile): def __init__(self, path, *args, **kwargs): diff --git a/apps/newtagging/models.py b/apps/newtagging/models.py index 6d05e287e..7e0f949f1 100644 --- a/apps/newtagging/models.py +++ b/apps/newtagging/models.py @@ -516,6 +516,7 @@ def create_intermediary_table_model(model): class Meta: db_table = '%s_relation' % model._meta.db_table unique_together = (('tag', 'content_type', 'object_id'),) + app_label = model._meta.app_label def obj_unicode(self): try: diff --git a/requirements-dev.txt b/requirements-dev.txt old mode 100755 new mode 100644 -- 2.20.1