Fixes #4076: Recommendations from collections.
[wolnelektury.git] / src / newtagging / models.py
index 694f5b8..e3e8f3c 100644 (file)
@@ -1,21 +1,20 @@
-# -*- 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.
 """
 
 """
 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.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.db.models.base import ModelBase
-from django.core.exceptions import ObjectDoesNotExist
 from django.dispatch import Signal
 
 qn = connection.ops.quote_name
 
 from django.dispatch import Signal
 
 qn = connection.ops.quote_name
 
-
 tags_updated = Signal(providing_args=["affected_tags"])
 
 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
 def get_queryset_and_model(queryset_or_model):
     """
     Given a ``QuerySet`` or a ``Model``, returns a two-tuple of
@@ -27,18 +26,21 @@ def get_queryset_and_model(queryset_or_model):
     try:
         return queryset_or_model, queryset_or_model.model
     except AttributeError:
     try:
         return queryset_or_model, queryset_or_model.model
     except AttributeError:
-        return queryset_or_model._default_manager.all(), queryset_or_model
+        return queryset_or_model.objects.all(), queryset_or_model
 
 
 ############
 # Managers #
 ############
 class TagManager(models.Manager):
 
 
 ############
 # Managers #
 ############
 class TagManager(models.Manager):
-    def __init__(self, intermediary_table_model):
+    def __init__(self):
         super(TagManager, self).__init__()
         super(TagManager, self).__init__()
-        self.intermediary_table_model = intermediary_table_model
         models.signals.pre_delete.connect(self.target_deleted)
 
         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:
     def target_deleted(self, instance, **kwargs):
         """ clear tag relations before deleting an object """
         try:
@@ -55,21 +57,22 @@ 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))
         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
 
         # Remove tags which no longer apply
-        tags_for_removal = [tag for tag in current_tags \
-                            if tag not in updated_tags]
+        tags_for_removal = [tag for tag in current_tags if tag not in updated_tags]
         if len(tags_for_removal):
         if len(tags_for_removal):
-            self.intermediary_table_model._default_manager.filter(content_type__pk=content_type.pk,
-                                               object_id=obj.pk,
-                                               tag__in=tags_for_removal).delete()
+            self.intermediary_table_model.objects.filter(
+                content_type__pk=content_type.pk,
+                object_id=obj.pk,
+                tag__in=tags_for_removal).delete()
         # Add new tags
         # 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:
         for tag in tags_to_add:
-            if tag not in current_tags:
-                self.intermediary_table_model._default_manager.create(tag=tag, content_object=obj)
+            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)
 
 
         tags_updated.send(sender=type(obj), instance=obj, affected_tags=tags_to_add + tags_for_removal)
 
@@ -78,8 +81,18 @@ class TagManager(models.Manager):
         Remove tag from an object.
         """
         content_type = ContentType.objects.get_for_model(obj)
         Remove tag from an object.
         """
         content_type = ContentType.objects.get_for_model(obj)
-        self.intermediary_table_model._default_manager.filter(content_type__pk=content_type.pk,
-            object_id=obj.pk, tag=tag).delete()
+        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):
         """
 
     def get_for_object(self, obj):
         """
@@ -105,9 +118,10 @@ class TagManager(models.Manager):
         ``filters`` argument.
         """
         # TODO: Do we really need this filters stuff?
         ``filters`` argument.
         """
         # TODO: Do we really need this filters stuff?
-        if filters is None: filters = {}
+        if filters is None:
+            filters = {}
 
 
-        queryset = model._default_manager.filter()
+        queryset = model.objects.filter()
         for f in filters.items():
             queryset.query.add_filter(f)
         usage = self.usage_for_queryset(queryset, counts)
         for f in filters.items():
             queryset.query.add_filter(f)
         usage = self.usage_for_queryset(queryset, counts)
@@ -122,10 +136,9 @@ class TagManager(models.Manager):
         each tag, indicating how many times it has been used against
         the Model class in question.
         """
         each tag, indicating how many times it has been used against
         the Model class in question.
         """
-        usage = self.model._default_manager.filter(
+        usage = self.model.objects.filter(
             items__content_type=ContentType.objects.get_for_model(queryset.model),
             items__content_type=ContentType.objects.get_for_model(queryset.model),
-            items__object_id__in=queryset
-            )
+            items__object_id__in=queryset)
         if counts:
             usage = usage.annotate(count=models.Count('id'))
         else:
         if counts:
             usage = usage.annotate(count=models.Count('id'))
         else:
@@ -148,9 +161,9 @@ class TagManager(models.Manager):
 
 
 class TaggedItemManager(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):
         """
 
     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)
         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()
         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)
         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
         if not tags:
             return queryset.none()
         # TODO: presumes reverse generic relation
@@ -192,68 +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)
         # 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
-