From f1ec080b394326e35074c57e682789176cd3f244 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 22 Jun 2010 16:09:06 +0200 Subject: [PATCH] Fix tag counts (#692) Cache global tag counts in book_count Clean TagRelation table, don't leave orphaned relations Remove theme tags from books Reset cache fields on tags changes Some tests Minor fixes --- apps/catalogue/forms.py | 2 +- .../0009_chg_book_count__heavy_cleaning.py | 226 ++++++++++++++++++ apps/catalogue/models.py | 69 +++++- apps/catalogue/templatetags/catalogue_tags.py | 1 + apps/catalogue/tests/book_import.py | 2 +- apps/catalogue/tests/tags.py | 61 ++++- apps/catalogue/views.py | 27 ++- apps/newtagging/models.py | 19 +- .../templates/catalogue/main_page.html | 2 +- .../templates/catalogue/user_shelves.html | 2 +- 10 files changed, 383 insertions(+), 28 deletions(-) create mode 100644 apps/catalogue/migrations/0009_chg_book_count__heavy_cleaning.py diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py index d2178248e..e2f52b0f8 100644 --- a/apps/catalogue/forms.py +++ b/apps/catalogue/forms.py @@ -44,7 +44,7 @@ class ObjectSetsForm(forms.Form): self.fields['set_ids'] = forms.MultipleChoiceField( label=_('Shelves'), required=False, - choices=[(tag.id, "%s (%s)" % (tag.name, tag.book_count)) for tag in Tag.objects.filter(category='set', user=user)], + choices=[(tag.id, "%s (%s)" % (tag.name, tag.get_count())) for tag in Tag.objects.filter(category='set', user=user)], initial=[tag.id for tag in obj.tags.filter(category='set', user=user)], widget=forms.CheckboxSelectMultiple ) diff --git a/apps/catalogue/migrations/0009_chg_book_count__heavy_cleaning.py b/apps/catalogue/migrations/0009_chg_book_count__heavy_cleaning.py new file mode 100644 index 000000000..16ef3a364 --- /dev/null +++ b/apps/catalogue/migrations/0009_chg_book_count__heavy_cleaning.py @@ -0,0 +1,226 @@ +# encoding: 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 'Tag.book_count' + db.alter_column('catalogue_tag', 'book_count', self.gf('django.db.models.fields.IntegerField')(null=True)) + + if not db.dry_run: + from django.contrib.contenttypes.models import ContentType + from simplejson import loads, dumps + + manager = orm.TagRelation.objects + + def type_id(model): + return ContentType.objects.get_for_model(model).pk + + def tagged_with_any(model, tags): + object_ids = {} + for relation in manager.filter(content_type=type_id(model), tag__in=tags): + object_ids[relation.object_id] = 1 + return model.objects.filter(pk__in=object_ids.keys()) + + def get_tags(instance): + return [relation.tag for relation in manager.filter( + content_type=type_id(type(instance)), object_id=instance.pk).select_related()] + + def refresh_book_count(tag): + if tag.category == 'theme': + objects = tagged_with_any(orm.Fragment, [tag]).only() + else: + objects = tagged_with_any(orm.Book, [tag]).only('slug') + if tag.category != 'set': + # eliminate descendants + l_tags = orm.Tag.objects.filter(slug__in=['l-'+book.slug for book in objects]) + descendants_keys = [book.pk for book in tagged_with_any(orm.Book, l_tags)] + if descendants_keys: + objects = objects.exclude(pk__in=descendants_keys) + tag.book_count = objects.count() + tag.save() + + def refresh_tag_counter(book): + tags = {} + for child in book.children.all().order_by(): + for tag_pk, value in tag_counter(child).iteritems(): + tags[tag_pk] = tags.get(tag_pk, 0) + value + for tag in [tag for tag in get_tags(book) if tag.category not in ('book', 'theme', 'set')]: + tags[tag.pk] = 1 + book._tag_counter = dumps(tags) + book.save() + return tags + + def tag_counter(book): + if book._tag_counter is None: + return refresh_tag_counter(book) + return dict((int(k), v) for k, v in loads(book._tag_counter).iteritems()) + + def theme_counter(book): + if book._theme_counter is None: + tags = {} + l_tag = orm.Tag.objects.get(slug='l-'+book.slug) + for fragment in tagged_with_any(orm.Fragment, [l_tag]): + for tag in [tag for tag in get_tags(fragment) if tag.category=='theme']: + tags[tag.pk] = tags.get(tag.pk, 0) + 1 + book._theme_counter = dumps(tags) + book.save() + + + # remove orphaned relations + book_type_id = type_id(orm.Book) + book_ids = [b.pk for b in orm.Book.objects.all().only()] + manager.filter(content_type=book_type_id).exclude(object_id__in=book_ids).delete() + del book_ids + + fragment_type_id = type_id(orm.Fragment) + fragment_ids = [b.pk for b in orm.Fragment.objects.all().only()] + manager.filter(content_type=fragment_type_id).exclude(object_id__in=fragment_ids).delete() + del fragment_ids + + tag_ids = [t.pk for t in orm.Tag.objects.all().only()] + manager.exclude(tag__in=tag_ids).delete() + del tag_ids + + # remove theme tags for books + manager.filter(content_type=book_type_id).filter(tag__category='theme').delete() + + # reset count fields + for tag in orm.Tag.objects.exclude(category__in=('book', 'set')).iterator(): + refresh_book_count(tag) + for book in orm.Book.objects.all().iterator(): + theme_counter(book) + for book in orm.Book.objects.filter(parent=None).iterator(): + tag_counter(book) + + def backwards(self, orm): + + # Changing field 'Tag.book_count' + db.alter_column('catalogue_tag', 'book_count', self.gf('django.db.models.fields.IntegerField')()) + + + 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']", 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'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']", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + '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']", 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'catalogue.book': { + 'Meta': {'object_name': 'Book'}, + '_short_html': ('django.db.models.fields.TextField', [], {}), + '_short_html_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_tag_counter': ('catalogue.fields.JSONField', [], {'null': 'True'}), + '_theme_counter': ('catalogue.fields.JSONField', [], {'null': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'extra_info': ('catalogue.fields.JSONField', [], {}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mp3_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'odt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'ogg_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', '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': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}) + }, + 'catalogue.bookstub': { + 'Meta': {'object_name': 'BookStub'}, + 'author': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pd': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'translator': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'translator_death': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'object_name': 'Fragment'}, + '_short_html': ('django.db.models.fields.TextField', [], {}), + '_short_html_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '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': {'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'}, + 'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'death': ('django.db.models.fields.IntegerField', [], {'null': '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'}), + 'main_page': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.SlugField', [], {'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': {'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'] diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py index 37ea4e842..a7e04f1a2 100644 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models.py @@ -13,7 +13,7 @@ from django.utils.translation import get_language from django.core.urlresolvers import reverse from datetime import datetime -from newtagging.models import TagBase +from newtagging.models import TagBase, tags_updated from newtagging import managers from catalogue.fields import JSONField @@ -51,7 +51,7 @@ class Tag(TagBase): 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'), default=0, blank=False, null=False) + book_count = models.IntegerField(_('book count'), blank=False, null=True) death = models.IntegerField(_(u'year of death'), blank=True, null=True) gazeta_link = models.CharField(blank=True, max_length=240) wiki_link = models.CharField(blank=True, max_length=240) @@ -98,6 +98,27 @@ class Tag(TagBase): """ calculates the year of public domain entry for an author """ return self.death + 71 if self.death is not None else None + 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 + @staticmethod def get_tag_list(tags): if isinstance(tags, basestring): @@ -215,8 +236,11 @@ class Book(models.Model): def name(self): return self.title + def book_tag_slug(self): + return ('l-' + self.slug)[:120] + def book_tag(self): - slug = ('l-' + self.slug)[:120] + slug = self.book_tag_slug() book_tag, created = Tag.objects.get_or_create(slug=slug, category='book') if created: book_tag.name = self.title[:50] @@ -345,7 +369,7 @@ class Book(models.Model): tag.save() book_tags.append(tag) - book.tags = book_tags + book.tags = book_tags + book_shelves book_tag = book.book_tag() @@ -378,7 +402,6 @@ class Book(models.Model): # Extract fragments closed_fragments, open_fragments = html.extract_fragments(book.html_file.path) - book_themes = [] for fragment in closed_fragments.values(): text = fragment.to_string() short_text = '' @@ -400,11 +423,11 @@ class Book(models.Model): tag.save() themes.append(tag) new_fragment.save() - new_fragment.tags = set(list(book.tags) + themes + [book_tag]) - book_themes += themes + new_fragment.tags = set(book_tags + themes + [book_tag]) - book_themes = set(book_themes) - book.tags = list(book.tags) + list(book_themes) + book_shelves + # refresh cache + book.tag_counter + book.theme_counter book.save() return book @@ -421,6 +444,12 @@ class Book(models.Model): self.save(reset_short_html=False, refresh_mp3=False) return tags + def reset_tag_counter(self): + self._tag_counter = None + self.save(reset_short_html=False, refresh_mp3=False) + if self.parent: + self.parent.reset_tag_counter() + @property def tag_counter(self): if self._tag_counter is None: @@ -436,6 +465,12 @@ class Book(models.Model): self.save(reset_short_html=False, refresh_mp3=False) return tags + def reset_theme_counter(self): + self._theme_counter = None + self.save(reset_short_html=False, refresh_mp3=False) + if self.parent: + self.parent.reset_theme_counter() + @property def theme_counter(self): if self._theme_counter is None: @@ -503,3 +538,19 @@ class BookStub(models.Model): return self.title +def _tags_updated_handler(sender, affected_tags, **kwargs): + # reset tag global counter + Tag.objects.filter(pk__in=[tag.pk for tag in affected_tags]).update(book_count=None) + + # 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) + diff --git a/apps/catalogue/templatetags/catalogue_tags.py b/apps/catalogue/templatetags/catalogue_tags.py index 36a015ae6..25376f8c3 100644 --- a/apps/catalogue/templatetags/catalogue_tags.py +++ b/apps/catalogue/templatetags/catalogue_tags.py @@ -253,6 +253,7 @@ def tag_list(tags, choices=None): @register.inclusion_tag('catalogue/folded_tag_list.html') def folded_tag_list(tags, choices=None): + tags = [tag for tag in tags if tag.count] if choices is None: choices = [] some_tags_hidden = False diff --git a/apps/catalogue/tests/book_import.py b/apps/catalogue/tests/book_import.py index e5fa031d4..3cb94cb98 100644 --- a/apps/catalogue/tests/book_import.py +++ b/apps/catalogue/tests/book_import.py @@ -76,7 +76,7 @@ class BookImportLogicTests(WLTestCase): self.assertEqual(book.fragments.count(), 1) self.assertEqual(book.fragments.all()[0].text, u'

Ala ma kota

\n') - self.assert_(('theme', 'love') in [ (tag.category, tag.slug) for tag in book.tags ]) + self.assert_(('theme', 'love') in [ (tag.category, tag.slug) for tag in book.fragments.all()[0].tags ]) def test_book_replace_title(self): BOOK_TEXT = """""" diff --git a/apps/catalogue/tests/tags.py b/apps/catalogue/tests/tags.py index 00d11678d..1d257f9ab 100644 --- a/apps/catalogue/tests/tags.py +++ b/apps/catalogue/tests/tags.py @@ -183,6 +183,10 @@ class CleanTagRelationTests(WLTestCase): models.Book.objects.all().delete() cats = self.client.get('/katalog/k/').context['categories'] self.assertEqual(cats, {}) + self.assertEqual(models.Fragment.objects.all().count(), 0, + "orphaned fragments left") + self.assertEqual(models.Tag.intermediary_table_model.objects.all().count(), 0, + "orphaned TagRelation objects left") def test_deleted_tag(self): """ there should be no tag relations left after deleting tags """ @@ -190,6 +194,8 @@ class CleanTagRelationTests(WLTestCase): models.Tag.objects.all().delete() cats = self.client.get('/katalog/lektura/book/').context['categories'] self.assertEqual(cats, {}) + self.assertEqual(models.Tag.intermediary_table_model.objects.all().count(), 0, + "orphaned TagRelation objects left") class TestIdenticalTag(WLTestCase): @@ -217,10 +223,11 @@ class TestIdenticalTag(WLTestCase): """ there should be all related tags in relevant categories """ models.Book.from_text_and_meta(ContentFile(self.book_text), self.book_info) - cats = self.client.get('/katalog/lektura/tag/').context['categories'] - for category in 'author', 'kind', 'genre', 'epoch', 'theme': - self.assertTrue('tag' in [tag.slug for tag in cats[category]], + context = self.client.get('/katalog/lektura/tag/').context + for category in 'author', 'kind', 'genre', 'epoch': + self.assertTrue('tag' in [tag.slug for tag in context['categories'][category]], 'missing related tag for %s' % category) + self.assertTrue('tag' in [tag.slug for tag in context['book_themes']]) def test_qualified_url(self): models.Book.from_text_and_meta(ContentFile(self.book_text), self.book_info) @@ -230,3 +237,51 @@ class TestIdenticalTag(WLTestCase): self.assertEqual(1, len(context['object_list'])) self.assertNotEqual({}, context['categories']) self.assertFalse(cat in context['categories']) + + +class BookTagsTests(WLTestCase): + """ tests the /katalog/lektura/book/ page for related tags """ + + def setUp(self): + WLTestCase.setUp(self) + author1 = PersonStub(("Common",), "Man") + author2 = PersonStub(("Jim",), "Lazy") + + child_info = BookInfoStub(authors=(author1, author2), genre="ChildGenre", epoch='Epoch', kind="ChildKind", + **info_args(u"Child")) + parent_info = BookInfoStub(author=author1, genre="Genre", epoch='Epoch', kind="Kind", + parts=[child_info.url], + **info_args(u"Parent")) + + for info in child_info, parent_info: + book_text = """ + + Theme, %sTheme + Ala ma kota + + + """ % info.title.encode('utf-8') + book = models.Book.from_text_and_meta(ContentFile(book_text), info) + + def test_book_tags(self): + """ book should have own tags and whole tree's themes """ + + context = self.client.get('/katalog/lektura/parent/').context + + self.assertEqual([tag.name for tag in context['categories']['author']], + ['Common Man']) + self.assertEqual([tag.name for tag in context['categories']['kind']], + ['Kind']) + self.assertEqual([(tag.name, tag.count) for tag in context['book_themes']], + [('ChildTheme', 1), ('ParentTheme', 1), ('Theme', 2)]) + + def test_main_page_tags(self): + """ test main page tags and counts """ + + context = self.client.get('/katalog/').context + + self.assertEqual([(tag.name, tag.count) for tag in context['categories']['author']], + [('Jim Lazy', 1), ('Common Man', 1)]) + self.assertEqual([(tag.name, tag.count) for tag in context['fragment_tags']], + [('ChildTheme', 1), ('ParentTheme', 1), ('Theme', 2)]) + diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index dd9830c04..de9d0b227 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -50,11 +50,12 @@ def main_page(request): if request.user.is_authenticated(): shelves = models.Tag.objects.filter(category='set', user=request.user) new_set_form = forms.NewSetForm() - extra_where = "NOT catalogue_tag.category = 'set'" - tags = models.Tag.objects.usage_for_model(models.Book, counts=True, extra={'where': [extra_where]}) - fragment_tags = models.Tag.objects.usage_for_model(models.Fragment, counts=True, - extra={'where': ["catalogue_tag.category = 'theme'"] + [extra_where]}) + + tags = models.Tag.objects.exclude(category__in=('set', 'book')) + for tag in tags: + tag.count = tag.get_count() categories = split_tags(tags) + fragment_tags = categories.get('theme', []) form = forms.SearchForm() return render_to_response('catalogue/main_page.html', locals(), @@ -119,7 +120,7 @@ def tagged_object_list(request, tags=''): if shelf_tags: books = models.Book.tagged.with_all(shelf_tags).order_by() - l_tags = [book.book_tag() for book in books] + l_tags = models.Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books]) fragments = models.Fragment.tagged.with_any(l_tags, fragments) # newtagging goes crazy if we just try: @@ -139,7 +140,7 @@ def tagged_object_list(request, tags=''): objects = models.Book.tagged.with_all(tags).order_by() if not shelf_is_set: # eliminate descendants - l_tags = [book.book_tag() for book in objects] + l_tags = models.Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in objects]) descendants_keys = [book.pk for book in models.Book.tagged.with_any(l_tags)] if descendants_keys: objects = objects.exclude(pk__in=descendants_keys) @@ -203,8 +204,12 @@ def book_detail(request, slug): tags = list(book.tags.filter(~Q(category='set'))) categories = split_tags(tags) book_children = book.children.all().order_by('parent_number') - extra_where = "catalogue_tag.category = 'theme'" - book_themes = models.Tag.objects.related_for_model(book_tag, models.Fragment, counts=True, extra={'where': [extra_where]}) + + theme_counter = book.theme_counter + book_themes = models.Tag.objects.filter(pk__in=theme_counter.keys()) + for tag in book_themes: + tag.count = theme_counter[tag.pk] + extra_info = book.get_extra_info_value() form = forms.SearchForm() @@ -404,11 +409,11 @@ def book_sets(request, slug): new_shelves = [models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']] for shelf in [shelf for shelf in old_shelves if shelf not in new_shelves]: - shelf.book_count -= 1 + shelf.book_count = None shelf.save() for shelf in [shelf for shelf in new_shelves if shelf not in old_shelves]: - shelf.book_count += 1 + shelf.book_count = None shelf.save() book.tags = new_shelves + list(book.tags.filter(~Q(category='set') | ~Q(user=request.user))) @@ -434,7 +439,7 @@ def remove_from_shelf(request, shelf, book): if shelf in book.tags: models.Tag.objects.remove_tag(book, shelf) - shelf.book_count -= 1 + shelf.book_count = None shelf.save() return HttpResponse(_('Book was successfully removed from the shelf')) diff --git a/apps/newtagging/models.py b/apps/newtagging/models.py index 1c35254bb..ea2a41f8e 100644 --- a/apps/newtagging/models.py +++ b/apps/newtagging/models.py @@ -15,6 +15,7 @@ from django.db import connection, models from django.utils.translation import ugettext_lazy as _ from django.db.models.base import ModelBase from django.core.exceptions import ObjectDoesNotExist +from django.dispatch import Signal qn = connection.ops.quote_name @@ -24,6 +25,8 @@ except ImportError: parse_lookup = None +tags_updated = Signal(providing_args=["affected_tags"]) + def get_queryset_and_model(queryset_or_model): """ Given a ``QuerySet`` or a ``Model``, returns a two-tuple of @@ -45,6 +48,16 @@ class TagManager(models.Manager): def __init__(self, intermediary_table_model): super(TagManager, self).__init__() self.intermediary_table_model = intermediary_table_model + models.signals.pre_delete.connect(self.target_deleted) + + def target_deleted(self, instance, **kwargs): + """ clear tag relations before deleting an object """ + try: + int(instance.pk) + except ValueError: + return + + self.update_tags(instance, []) def update_tags(self, obj, tags): """ @@ -63,10 +76,14 @@ class TagManager(models.Manager): object_id=obj.pk, tag__in=tags_for_removal).delete() # Add new tags - for tag in updated_tags: + tags_to_add = [tag for tag in updated_tags + if tag not in current_tags] + for tag in tags_to_add: if tag not in current_tags: self.intermediary_table_model._default_manager.create(tag=tag, content_object=obj) + tags_updated.send(sender=obj, affected_tags=tags_to_add + tags_for_removal) + def remove_tag(self, obj, tag): """ Remove tag from an object. diff --git a/wolnelektury/templates/catalogue/main_page.html b/wolnelektury/templates/catalogue/main_page.html index 8fb3b9672..d84ed0089 100644 --- a/wolnelektury/templates/catalogue/main_page.html +++ b/wolnelektury/templates/catalogue/main_page.html @@ -21,7 +21,7 @@ {% if shelves %} {% else %} diff --git a/wolnelektury/templates/catalogue/user_shelves.html b/wolnelektury/templates/catalogue/user_shelves.html index 28c122295..d047dbe74 100644 --- a/wolnelektury/templates/catalogue/user_shelves.html +++ b/wolnelektury/templates/catalogue/user_shelves.html @@ -3,7 +3,7 @@ {% if shelves %} {% else %} -- 2.20.1