Minor fixes.
[wolnelektury.git] / src / newtagging / models.py
index 1d1b221..e3e8f3c 100644 (file)
@@ -1,14 +1,13 @@
-# -*- 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.
+#
 """
 Models and managers for generic tagging.
 """
 
-from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 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
@@ -34,11 +33,14 @@ def get_queryset_and_model(queryset_or_model):
 # Managers #
 ############
 class TagManager(models.Manager):
-    def __init__(self, intermediary_table_model):
+    def __init__(self):
         super(TagManager, self).__init__()
-        self.intermediary_table_model = intermediary_table_model
         models.signals.pre_delete.connect(self.target_deleted)
 
+    @property
+    def intermediary_table_model(self):
+        return self.model.intermediary_table_model
+
     def target_deleted(self, instance, **kwargs):
         """ clear tag relations before deleting an object """
         try:
@@ -55,7 +57,7 @@ class TagManager(models.Manager):
         content_type = ContentType.objects.get_for_model(obj)
         current_tags = list(self.filter(items__content_type__pk=content_type.pk,
                                         items__object_id=obj.pk))
-        updated_tags = self.model.get_tag_list(tags)
+        updated_tags = tags
 
         # Remove tags which no longer apply
         tags_for_removal = [tag for tag in current_tags if tag not in updated_tags]
@@ -65,10 +67,11 @@ class TagManager(models.Manager):
                 object_id=obj.pk,
                 tag__in=tags_for_removal).delete()
         # Add new tags
-        tags_to_add = [tag for tag in updated_tags
-                       if tag not in current_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:
+            existing = self.intermediary_table_model.objects.filter(
+                content_type__pk=content_type.pk, object_id=obj.pk, tag=tag)
+            if not existing:
                 self.intermediary_table_model.objects.create(tag=tag, content_object=obj)
 
         tags_updated.send(sender=type(obj), instance=obj, affected_tags=tags_to_add + tags_for_removal)
@@ -81,6 +84,16 @@ class TagManager(models.Manager):
         self.intermediary_table_model.objects.filter(
             content_type__pk=content_type.pk, object_id=obj.pk, tag=tag).delete()
 
+    def add_tag(self, obj, tag):
+        """
+        Add tag to an object.
+        """
+        content_type = ContentType.objects.get_for_model(obj)
+        relations = self.intermediary_table_model.objects.filter(
+            content_type__pk=content_type.pk, object_id=obj.pk, tag=tag)
+        if not relations:
+            self.intermediary_table_model.objects.create(tag=tag, content_object=obj)
+
     def get_for_object(self, obj):
         """
         Create a queryset matching all tags associated with the given
@@ -148,9 +161,9 @@ class TagManager(models.Manager):
 
 
 class TaggedItemManager(models.Manager):
-    def __init__(self, tag_model):
-        super(TaggedItemManager, self).__init__()
-        self.tag_model = tag_model
+    @property
+    def tag_model(self):
+        return self.model.tag_model
 
     def get_by_model(self, queryset_or_model, tags):
         """
@@ -158,7 +171,6 @@ class TaggedItemManager(models.Manager):
         model associated with a given tag or list of tags.
         """
         queryset, model = get_queryset_and_model(queryset_or_model)
-        tags = self.tag_model.get_tag_list(tags)
         if not tags:
             # No existing tags were given
             return queryset.none()
@@ -175,7 +187,6 @@ class TaggedItemManager(models.Manager):
         model associated with *any* of the given list of tags.
         """
         queryset, model = get_queryset_and_model(queryset_or_model)
-        tags = self.tag_model.get_tag_list(tags)
         if not tags:
             return queryset.none()
         # TODO: presumes reverse generic relation
@@ -192,67 +203,3 @@ class TaggedItemManager(models.Manager):
         # Do we know it's 'tags'?
         return queryset.filter(tag_relations__tag__in=obj.tags).annotate(
             count=models.Count('pk')).order_by('-count').exclude(pk=obj.pk)
-
-
-##########
-# Models #
-##########
-def create_intermediary_table_model(model):
-    """Create an intermediary table model for the specific tag model"""
-    name = model.__name__ + 'Relation'
-
-    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:
-            return u'%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
-        except ObjectDoesNotExist:
-            return u'<deleted> [%s]' % self.tag
-
-    # Set up a dictionary to simulate declarations within a class
-    attrs = {
-        '__module__': model.__module__,
-        'Meta': Meta,
-        'tag': models.ForeignKey(model, verbose_name=_('tag'), related_name='items'),
-        'content_type': models.ForeignKey(ContentType, verbose_name=_('content type')),
-        'object_id': models.PositiveIntegerField(_('object id'), db_index=True),
-        'content_object': GenericForeignKey('content_type', 'object_id'),
-        '__unicode__': obj_unicode,
-    }
-
-    return type(name, (models.Model,), attrs)
-
-
-class TagMeta(ModelBase):
-    """Metaclass for tag models (models inheriting from TagBase)."""
-    def __new__(mcs, name, bases, attrs):
-        model = super(TagMeta, mcs).__new__(mcs, name, bases, attrs)
-        if not model._meta.abstract:
-            # Create an intermediary table and register custom managers for concrete models
-            model.intermediary_table_model = create_intermediary_table_model(model)
-            TagManager(model.intermediary_table_model).contribute_to_class(model, 'objects')
-            TaggedItemManager(model).contribute_to_class(model.intermediary_table_model, 'objects')
-        return model
-
-
-class TagBase(models.Model):
-    """Abstract class to be inherited by model classes."""
-    __metaclass__ = TagMeta
-
-    class Meta:
-        abstract = True
-
-    @staticmethod
-    def get_tag_list(tag_list):
-        """
-        Utility function for accepting tag input in a flexible manner.
-
-        You should probably override this method in your subclass.
-        """
-        if isinstance(tag_list, TagBase):
-            return [tag_list]
-        else:
-            return tag_list