Add set_base() support to CustomTaggableManager so Django's deserializer can restore tag relationships through django-taggit. Normalize primary-key values before delegating to taggit and add regression coverage for object- and PK-based inputs.
This commit is contained in:
parent
875e7d0885
commit
d7f79bd501
|
|
@ -11,8 +11,11 @@ __all__ = (
|
|||
|
||||
class NetBoxTaggableManager(_TaggableManager):
|
||||
"""
|
||||
Extends taggit's _TaggableManager to replace the per-tag get_or_create loop in add() with a
|
||||
single bulk_create() call, reducing SQL queries from O(N) to O(1) when assigning tags.
|
||||
Extends taggit's _TaggableManager to:
|
||||
|
||||
* Replace the per-tag get_or_create loop in add() with a single bulk_create() call, reducing
|
||||
SQL queries from O(N) to O(1) when assigning tags.
|
||||
* Implement set_base(), the M2M assignment entry point Django's deserializer calls.
|
||||
"""
|
||||
|
||||
@require_instance_manager
|
||||
|
|
@ -67,6 +70,21 @@ class NetBoxTaggableManager(_TaggableManager):
|
|||
using=db,
|
||||
)
|
||||
|
||||
@require_instance_manager
|
||||
def set_base(self, objs, *, clear=False, through_defaults=None, raw=False):
|
||||
# Django's deserializer assigns M2M data through this method, passing primary keys;
|
||||
# taggit's set() takes only Tag instances or names. Keys which match no tag are passed
|
||||
# through for the database to reject, as ManyRelatedManager.set_base() does.
|
||||
tag_model = self.through.tag_model()
|
||||
if pks := [obj for obj in objs if not isinstance(obj, (tag_model, str))]:
|
||||
db = router.db_for_write(self.through, instance=self.instance)
|
||||
tags = tag_model._default_manager.using(db).in_bulk(pks)
|
||||
objs = [
|
||||
obj if isinstance(obj, (tag_model, str)) else tags.get(obj) or tag_model(pk=obj)
|
||||
for obj in objs
|
||||
]
|
||||
return self.set(objs, clear=clear, through_defaults=through_defaults)
|
||||
|
||||
|
||||
class NetBoxTaggableManagerField(TaggableManager):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from django.core import serializers
|
||||
from django.db import IntegrityError, connection, transaction
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.choices import SiteStatusChoices
|
||||
|
|
@ -58,3 +60,55 @@ class SerializationTestCase(TestCase):
|
|||
self.assertEqual(instance.object.status, SiteStatusChoices.STATUS_ACTIVE) # Default field value
|
||||
self.assertEqual(instance.object.foo, data['foo']) # Non-field attribute
|
||||
self.assertEqual(list(instance.m2m_data['tags']), list(Tag.objects.all()))
|
||||
|
||||
def test_deserialized_object_save(self):
|
||||
"""A deserialized object saves and reassigns its tags (Django 6.1 calls set_base())."""
|
||||
data = {
|
||||
'name': 'Site 1',
|
||||
'slug': 'site-1',
|
||||
'tags': ['Tag 1', 'Tag 2'],
|
||||
}
|
||||
deserialize_object(Site, data, pk=123).save()
|
||||
|
||||
site = Site.objects.get(pk=123)
|
||||
self.assertEqual(site.name, data['name'])
|
||||
self.assertEqual(sorted(site.tags.values_list('name', flat=True)), data['tags'])
|
||||
|
||||
def test_deserialized_object_save_with_empty_tag_list(self):
|
||||
"""
|
||||
An empty `tags` list still reaches set_base(): deserialize_object() only pops the key
|
||||
when it is non-empty, so Django populates m2m_data['tags'] with an empty list. (Data
|
||||
carrying no `tags` key at all would not populate m2m_data, and so would not exercise
|
||||
set_base() — keep the empty list explicit.)
|
||||
"""
|
||||
deserialize_object(Site, {'name': 'Site 2', 'slug': 'site-2', 'tags': []}, pk=124).save()
|
||||
|
||||
site = Site.objects.get(pk=124)
|
||||
self.assertEqual(site.name, 'Site 2')
|
||||
self.assertEqual(site.tags.count(), 0)
|
||||
|
||||
def test_deserialized_object_save_with_tag_pks(self):
|
||||
"""Django's deserializer resolves M2M values to primary keys; set_base() must take them."""
|
||||
tags = list(Tag.objects.all()[:2])
|
||||
data = [{
|
||||
'model': 'dcim.site',
|
||||
'pk': 125,
|
||||
'fields': {'name': 'Site 3', 'slug': 'site-3', 'tags': [tag.pk for tag in tags]},
|
||||
}]
|
||||
list(serializers.deserialize('python', data))[0].save()
|
||||
|
||||
site = Site.objects.get(pk=125)
|
||||
self.assertEqual(sorted(site.tags.values_list('pk', flat=True)), sorted(tag.pk for tag in tags))
|
||||
|
||||
def test_deserialized_object_save_with_unknown_tag_pk(self):
|
||||
"""A key matching no tag is left for the database to reject."""
|
||||
data = [{
|
||||
'model': 'dcim.site',
|
||||
'pk': 126,
|
||||
'fields': {'name': 'Site 4', 'slug': 'site-4', 'tags': [99999]},
|
||||
}]
|
||||
with transaction.atomic():
|
||||
list(serializers.deserialize('python', data))[0].save()
|
||||
with self.assertRaises(IntegrityError):
|
||||
connection.check_constraints()
|
||||
transaction.set_rollback(True)
|
||||
|
|
|
|||
Loading…
Reference in New Issue