Fix tag counts (#692)
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 22 Jun 2010 14:09:06 +0000 (16:09 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 22 Jun 2010 14:23:33 +0000 (16:23 +0200)
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
apps/catalogue/migrations/0009_chg_book_count__heavy_cleaning.py [new file with mode: 0644]
apps/catalogue/models.py
apps/catalogue/templatetags/catalogue_tags.py
apps/catalogue/tests/book_import.py
apps/catalogue/tests/tags.py
apps/catalogue/views.py
apps/newtagging/models.py
wolnelektury/templates/catalogue/main_page.html
wolnelektury/templates/catalogue/user_shelves.html

index d217824..e2f52b0 100644 (file)
@@ -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 (file)
index 0000000..16ef3a3
--- /dev/null
@@ -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']
index 37ea4e8..a7e04f1 100644 (file)
@@ -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)
+
index 36a015a..25376f8 100644 (file)
@@ -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
index e5fa031..3cb94cb 100644 (file)
@@ -76,7 +76,7 @@ class BookImportLogicTests(WLTestCase):
         self.assertEqual(book.fragments.count(), 1)
         self.assertEqual(book.fragments.all()[0].text, u'<p class="paragraph">Ala ma kota</p>\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 = """<utwor />"""
index 00d1167..1d257f9 100644 (file)
@@ -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 = """<utwor><opowiadanie><akap>
+                <begin id="m01" />
+                    <motyw id="m01">Theme, %sTheme</motyw>
+                    Ala ma kota
+                <end id="m01" />
+                </akap></opowiadanie></utwor>
+                """ % 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)])
+
index dd9830c..de9d0b2 100644 (file)
@@ -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'))
index 1c35254..ea2a41f 100644 (file)
@@ -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.
index 8fb3b96..d84ed00 100644 (file)
@@ -21,7 +21,7 @@
                 {% if shelves %}
                 <ul class="shelf-list">
                 {% for shelf in shelves %}
-                    <li><a href="{% url delete_shelf shelf.slug %}" class="delete-shelf">{% trans "delete" %}</a> <a href="{{ shelf.get_absolute_url }}" class="visit-shelf">{{ shelf.name }} ({{ shelf.book_count }})</a></li>
+                    <li><a href="{% url delete_shelf shelf.slug %}" class="delete-shelf">{% trans "delete" %}</a> <a href="{{ shelf.get_absolute_url }}" class="visit-shelf">{{ shelf.name }} ({{ shelf.get_count }})</a></li>
                 {% endfor %}
                 </ul>
                 {% else %}
index 28c1222..d047dbe 100644 (file)
@@ -3,7 +3,7 @@
 {% if shelves %}
 <ul class="shelf-list">
 {% for shelf in shelves %}
-    <li><a href="{% url delete_shelf shelf.slug %}" class="delete-shelf">{% trans "remove" %}</a> <a href="{{ shelf.get_absolute_url }}" class="visit-shelf">{{ shelf.name }} ({{ shelf.book_count }})</a></li>
+    <li><a href="{% url delete_shelf shelf.slug %}" class="delete-shelf">{% trans "remove" %}</a> <a href="{{ shelf.get_absolute_url }}" class="visit-shelf">{{ shelf.name }} ({{ shelf.get_count }})</a></li>
 {% endfor %}
 </ul>
 {% else %}