Skip to content

Commit

Permalink
Fixed inheritance
Browse files Browse the repository at this point in the history
  • Loading branch information
radiac committed Apr 29, 2020
1 parent 7ee467a commit a0a7fd8
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 102 deletions.
12 changes: 0 additions & 12 deletions tagulous/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,6 @@ def contribute_to_class(self, cls, name):

# Create a new tag model if we need to
if self.auto_tag_model:
# Make sure a TagField is only contributed once if the model is
# not explicitly set. This isn't normal for model fields, but in
# this case the name of the model (and therefore db) would depend
# on the load order, which could change. Rather than risk problems
# later, ban it outright to save developers from themselves.
# If it causes problems for anyone, they can explicitly set a tag
# model and avoid this being a problem.
if self.contributed:
raise AttributeError(
"The tag field %r is already attached to a model" % self
)

# Generate a list of attributes for the new tag model
model_attrs = {
# Module should be the same as the main model
Expand Down
11 changes: 9 additions & 2 deletions tagulous/models/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,15 @@ def post_delete_handler(self):
# Decrement the actual tag
old_tag = self.get_actual()
if old_tag:
old_tag.decrement()

#### TODO: This probalby shouldn't happen at all. Why is it happening?
#### decrement() must be getting called twice?

try:
old_tag.decrement()
except type(old_tag).DoesNotExist:
# The object was deleted as part of this operation
pass
self.set_actual(None)

# If there is no new value, mark the old one as a new one,
Expand Down Expand Up @@ -656,4 +664,3 @@ def clear(self):
tag.decrement()
self.tags = []
clear.alters_data = True

138 changes: 138 additions & 0 deletions tagulous/models/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""
Model signal handlers
This really should be tagulous.signals, imported by the tagulous app's ready()
call, as per the django docs [1] - but that's only available in Django 1.7 or
later and we're supporting back to 1.4, so we'll keep it in here for now.
[1] https://docs.djangoproject.com/en/1.10/topics/signals/#connecting-receiver-functions
"""
from django.db import models

from tagulous import settings
from tagulous.models.fields import SingleTagField, TagField
from tagulous.models.tagged import TaggedModel


if settings.ENHANCE_MODELS:

def class_prepared_listener(sender, **kwargs):
"""
Listen to the class_prepared signal and subclass any model with tag
fields
"""
TaggedModel.cast_class(sender)

models.signals.class_prepared.connect(class_prepared_listener, weak=False)


class TaggedSignalHandler(object):
"""
Base class handling signals from TaggedModel subclasses
"""

def __call__(self, sender, instance, **kwargs):
if not self.is_relevant(sender):
return

for field, field_type in self.get_fields(sender):
descriptor = getattr(sender, field.name)
manager = descriptor.get_manager(instance)

self.handle(manager, field_type, kwargs.get("raw", False))

def is_relevant(self, sender):
return issubclass(sender, TaggedModel)

def get_fields(self, sender):
"""
Generator which returns (field, field_type) pairs
"""
for field in sender._meta.get_fields():
if isinstance(field, SingleTagField):
yield (field, SingleTagField)
elif isinstance(field, TagField):
yield (field, TagField)

def handle(self, manager, field_type, is_raw):
raise NotImplementedError()


class PreSaveHandler(TaggedSignalHandler):
"""
Pre-save signal handler
Make sure SingleTagField is in correct format to be saved. The manager needs to know
when the model is about to be saved # so that it can ensure the tag exists and
assign its pk to field_id.
"""

def handle(self, manager, field_type, is_raw):
if field_type != SingleTagField:
return
manager.pre_save_handler()


class PostSaveHandler(TaggedSignalHandler):
"""
Post-save signal handler
Ensure both tag fields' states are saved
"""

def handle(self, manager, field_type, is_raw):
manager.post_save_handler()

# If raw is set, data is being injected into the system, most likely from a
# deserialization operation. If the tag model has just been deserialized too,
# the tag counts will probably be off.
if is_raw:
if field_type == SingleTagField:
manager.get().update_count()
else:
for tag in manager.tags:
tag.update_count()


class PropagatedSignalMixin(object):
def get_fields(self, sender):
"""
Post delete signal propagates up the class hierarchy, so filter fields
returned to those belonging to the model which raised this signal
"""
for field, field_type in super(PropagatedSignalMixin, self).get_fields(sender):
if field.model != sender:
continue
yield (field, field_type)


class PreDeleteHandler(PropagatedSignalMixin, TaggedSignalHandler):
"""
Pre-delete signal handler
Ensure TagField is cleaned out before the main object is deleted
"""

def handle(self, manager, field_type, is_raw):
if field_type != TagField:
return
manager.pre_delete_handler()


class PostDeleteHandler(PropagatedSignalMixin, TaggedSignalHandler):
"""
Post-delete signal handler
Update tag count on delete
"""

def handle(self, manager, field_type, is_raw):
if field_type != SingleTagField:
return
manager.post_delete_handler()


models.signals.pre_save.connect(PreSaveHandler(), weak=False)
models.signals.post_save.connect(PostSaveHandler(), weak=False)
models.signals.pre_delete.connect(PreDeleteHandler(), weak=False)
models.signals.post_delete.connect(PostDeleteHandler(), weak=False)
55 changes: 48 additions & 7 deletions tests/tagulous_tests_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,29 @@ class SingleTagFieldModel(models.Model):
title = tagulous.models.SingleTagField(blank=True, null=True)

class SingleTagFieldConcreteInheritanceModel(SingleTagFieldModel):
pass
"""
Test concrete inheritance of a SingleTagField
"""

class SingleTagFieldAbstractInheritanceBase(models.Model):
"""
Abstract base class for SingleTagFieldAbstractInheritanceModel
"""
name = models.CharField(blank=True, max_length=100)
title = tagulous.models.SingleTagField(blank=True, null=True)

class Meta:
abstract = True

class SingleTagFieldAbstractInheritanceModel(SingleTagFieldAbstractInheritanceBase):
"""
Test abstract inheritance of a SingleTagField
"""

class SingleTagFieldAbstractInheritanceSecondModel(SingleTagFieldAbstractInheritanceBase):
"""
Test second abstract inheritance of a SingleTagField
"""

class SingleTagFieldOptionalModel(models.Model):
"""
Expand Down Expand Up @@ -168,6 +190,31 @@ class TagFieldModel(models.Model):
name = models.CharField(blank=True, max_length=100)
tags = tagulous.models.TagField()

class TagFieldConcreteInheritanceModel(TagFieldModel):
"""
Test concrete inheritance of a TagField
"""

class TagFieldAbstractInheritanceBase(models.Model):
"""
Abstract base class for AbstractTagFieldModel
"""
name = models.CharField(blank=True, max_length=100)
tags = tagulous.models.TagField()

class Meta:
abstract = True

class TagFieldAbstractInheritanceModel(TagFieldAbstractInheritanceBase):
"""
Test abstract inheritance of a TagField
"""

class TagFieldAbstractInheritanceSecondModel(TagFieldAbstractInheritanceBase):
"""
Test second abstract inheritance of a TagField
"""

class TagFieldOptionalModel(models.Model):
"""
Test optional tag fields
Expand Down Expand Up @@ -272,12 +319,6 @@ class SimpleMixedTest(models.Model):
singletag = tagulous.models.SingleTagField(blank=True)
tags = tagulous.models.TagField(blank=True)

class ConcreteInheritanceTest(SimpleMixedTest):
"""
For testing concrete inheritance
"""
pass

class MixedTestTagModel(tagulous.models.TagModel):
class TagMeta:
get_absolute_url = lambda self: 'url for %s' % self
Expand Down
63 changes: 55 additions & 8 deletions tests/test_models_singletagfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
tagulous.models.fields.BaseTagField
tagulous.models.fields.SingleTagField
"""
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals

from django.utils import six

Expand Down Expand Up @@ -221,9 +220,7 @@ def test_delete_decreases_count(self):
self.assertTagModel(self.tag_model, {
'Mr': 2,
})
print('DEL expecting to del Mr >>>>>>>>')
t2.delete()
print('<<<<<<<<<')
self.assertInstanceEqual(t1, name='Test 1', title='Mr')
self.assertTagModel(self.tag_model, {
'Mr': 1,
Expand Down Expand Up @@ -262,9 +259,7 @@ def test_delete_decreases_correct(self):
'Mr': 1,
'Mrs': 1,
})
print('DEL expecting to del Mr >>>>>>>>>')
t1.delete()
print('<<<<<<<<<')

# Check the original tag 'Mr' was decremented (and deleted)
self.assertTagModel(self.tag_model, {
Expand All @@ -283,9 +278,7 @@ def test_save_deleted_instance(self):
self.assertTagModel(self.tag_model, {
'Mr': 1,
})
print('DEL expecting to del Mr >>>>>>>>>>>>')
t1.delete()
print('<<<<<<<<<<<<')
self.assertTagModel(self.tag_model, {})
t1.save()
self.assertTagModel(self.tag_model, {
Expand All @@ -301,6 +294,7 @@ def test_save_deleted_tag(self):
self.assertTagModel(self.tag_model, {
'Mr': 1,
})

self.tag_model.objects.all().delete()
self.assertTagModel(self.tag_model, {})

Expand Down Expand Up @@ -392,6 +386,59 @@ def setUpExtra(self):
self.tag_model = test_models.SingleTagFieldConcreteInheritanceModel.title.tag_model
self.tag_field = test_models.SingleTagFieldConcreteInheritanceModel.title

def test_save_deleted_instance(self):
"""
Disable this test as it doesn't make sense in the context of a concrete
subclass; when the subclass is deleted, it will delete the base model instance,
meaning when we go to save it again we'll get the error:
save() prohibited to prevent data loss due to unsaved related object ..._ptr
where ..._ptr is a OneToOneField to the base model. This is expected behaviour,
and out of scope for this package to address.
"""


###############################################################################
####### Test it works with abstract inheritance
###############################################################################

class ModelSingleTagFieldAbstractInheritanceTest(ModelSingleTagFieldConcreteInheritanceTest):
manage_models = [
test_models.SingleTagFieldAbstractInheritanceModel,
]

def setUpExtra(self):
self.test_model = test_models.SingleTagFieldAbstractInheritanceModel
self.tag_model = test_models.SingleTagFieldAbstractInheritanceModel.title.tag_model
self.tag_field = test_models.SingleTagFieldAbstractInheritanceModel.title

def test_second_inheritance__tag_model_not_shared(self):
second_test_model = test_models.SingleTagFieldAbstractInheritanceSecondModel
second_tag_model = second_test_model.title.tag_model
self.assertNotEqual(self.tag_model, second_tag_model)

def test_second_inheritance__writes_update_shared_model(self):
second_test_model = test_models.SingleTagFieldAbstractInheritanceSecondModel
second_tag_model = second_test_model.title.tag_model

t1 = self.test_model(name="Test 1", title='Mr')
t1.save()

t2 = second_test_model(name="Test 2", title="Mrs")
t2.save()

self.assertEqual(t1.name, 'Test 1')
self.assertEqual(t1.title.name, 'Mr')
self.assertEqual(t2.name, 'Test 2')
self.assertEqual(t2.title.name, 'Mrs')
self.assertTagModel(self.tag_model, {
'Mr': 1,
})
self.assertTagModel(second_tag_model, {
'Mrs': 1,
})


###############################################################################
####### Test invalid fields
Expand Down

0 comments on commit a0a7fd8

Please sign in to comment.