from __future__ import unicode_literals from __future__ import absolute_import from taggit import VERSION as TAGGIT_VERSION from taggit.managers import TaggableManager, _TaggableManager from taggit.utils import require_instance_manager from modelcluster.queryset import FakeQuerySet if TAGGIT_VERSION < (0, 20, 0): raise Exception("modelcluster.contrib.taggit requires django-taggit version 0.20 or above") class _ClusterTaggableManager(_TaggableManager): @require_instance_manager def get_tagged_item_manager(self): """Return the manager that handles the relation from this instance to the tagged_item class. If content_object on the tagged_item class is defined as a ParentalKey, this will be a DeferringRelatedManager which allows writing related objects without committing them to the database. """ rel_name = self.through._meta.get_field('content_object').remote_field.get_accessor_name() return getattr(self.instance, rel_name) def get_queryset(self, extra_filters=None): if self.instance is None: # this manager is not associated with a specific model instance # (which probably means it's being invoked within a prefetch_related operation); # this means that we don't have to deal with uncommitted models/tags, and can just # use the standard taggit handler return super(_ClusterTaggableManager, self).get_queryset(extra_filters) else: # FIXME: we ought to have some way of querying the tagged item manager about whether # it has uncommitted changes, and return a real queryset (using the original taggit logic) # if not return FakeQuerySet( self.through.tag_model(), [tagged_item.tag for tagged_item in self.get_tagged_item_manager().all()] ) @require_instance_manager def add(self, *tags): if TAGGIT_VERSION >= (1, 3, 0): tag_objs = self._to_tag_model_instances(tags, {}) else: tag_objs = self._to_tag_model_instances(tags) # Now write these to the relation tagged_item_manager = self.get_tagged_item_manager() for tag in tag_objs: if not tagged_item_manager.filter(tag=tag): # make an instance of the self.through model and add it to the relation tagged_item = self.through(tag=tag) tagged_item_manager.add(tagged_item) @require_instance_manager def remove(self, *tags): tagged_item_manager = self.get_tagged_item_manager() tagged_items = [ tagged_item for tagged_item in tagged_item_manager.all() if tagged_item.tag.name in tags ] tagged_item_manager.remove(*tagged_items) @require_instance_manager def set(self, *tags, **kwargs): # Ignore the 'clear' kwarg (which defaults to False) and override it to be always true; # this means that set is implemented as a clear then an add, which was the standard behaviour # prior to django-taggit 0.19 (https://github.com/alex/django-taggit/commit/6542a702b590a5cfb91ea0de218b7f71ffd07c33). # # In this way, we avoid a live database lookup that occurs in the clear=False branch. # # The clear=True behaviour is fine for our purposes; the distinction only exists in django-taggit # to ensure that the correct set of m2m_changed signals is fired, and our reimplementation here # doesn't fire them at all (which makes logical sense, because the whole point of this module is # that the add/remove/set/clear operations don't write to the database). return super(_ClusterTaggableManager, self).set(*tags, clear=True) @require_instance_manager def clear(self): self.get_tagged_item_manager().clear() class ClusterTaggableManager(TaggableManager): _need_commit_after_assignment = True def __get__(self, instance, model): # override TaggableManager's requirement for instance to have a primary key # before we can access its tags manager = _ClusterTaggableManager( through=self.through, model=model, instance=instance, prefetch_cache_name=self.name ) return manager def value_from_object(self, instance): # retrieve the queryset via the related manager on the content object, # to accommodate the possibility of this having uncommitted changes relative to # the live database rel_name = self.through._meta.get_field('content_object').remote_field.get_accessor_name() ret = getattr(instance, rel_name).all() if TAGGIT_VERSION >= (1, ): # expects a Tag list instead of TaggedItem List ret = [tagged_item.tag for tagged_item in ret] return ret