Skip to content

Commit 0ba86fe

Browse files
committed
feat(models): Handle GFK attributes in CloningMixin
Extend the CloningMixin to inject GenericForeignKey (GFK) attributes when both content type and ID fields are present. Improves support for models using GFK fields during cloning operations. Fixes #21201
1 parent 8e620ef commit 0ba86fe

File tree

2 files changed

+69
-2
lines changed

2 files changed

+69
-2
lines changed

netbox/netbox/models/features.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from collections import defaultdict
33
from functools import cached_property
44

5-
from django.contrib.contenttypes.fields import GenericRelation
5+
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
66
from django.contrib.contenttypes.models import ContentType
77
from django.core.validators import ValidationError
88
from django.db import models
@@ -159,6 +159,13 @@ def clone(self):
159159
elif field_value not in (None, ''):
160160
attrs[field_name] = field_value
161161

162+
# Handle GenericForeignKeys. If the CT and ID fields are being cloned, also
163+
# include the name of the GFK attribute itself, as this is what forms expect.
164+
for field in self._meta.private_fields:
165+
if isinstance(field, GenericForeignKey):
166+
if field.ct_field in attrs and field.fk_field in attrs:
167+
attrs[field.name] = attrs[field.fk_field]
168+
162169
# Include tags (if applicable)
163170
if is_taggable(self):
164171
attrs['tags'] = [tag.pk for tag in self.tags.all()]

netbox/netbox/tests/test_model_features.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
1+
from unittest import skipIf
2+
3+
from django.conf import settings
14
from django.test import TestCase
25

36
from core.models import AutoSyncRecord, DataSource
7+
from dcim.models import Site
48
from extras.models import CustomLink
9+
from ipam.models import Prefix
510
from netbox.models.features import get_model_features, has_feature, model_is_public
6-
from netbox.tests.dummy_plugin.models import DummyModel
711
from taggit.models import Tag
812

913

1014
class ModelFeaturesTestCase(TestCase):
15+
"""
16+
A test case class for verifying model features and utility functions.
17+
"""
1118

19+
@skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, 'dummy_plugin not in settings.PLUGINS')
1220
def test_model_is_public(self):
1321
"""
1422
Test that the is_public() utility function returns True for public models only.
1523
"""
24+
from netbox.tests.dummy_plugin.models import DummyModel
25+
1626
# Public model
1727
self.assertFalse(hasattr(DataSource, '_netbox_private'))
1828
self.assertTrue(model_is_public(DataSource))
@@ -51,3 +61,53 @@ def test_get_model_features(self):
5161
features = get_model_features(CustomLink)
5262
self.assertIn('cloning', features)
5363
self.assertNotIn('bookmarks', features)
64+
65+
def test_cloningmixin_injects_gfk_attribute(self):
66+
"""
67+
Tests the cloning mixin with GFK attribute injection in the `clone` method.
68+
69+
This test validates that the `clone` method correctly handles
70+
and retains the General Foreign Key (GFK) attributes on an
71+
object when the cloning fields are explicitly defined.
72+
"""
73+
site = Site.objects.create(name='Test Site', slug='test-site')
74+
prefix = Prefix.objects.create(prefix='10.0.0.0/24', scope=site)
75+
76+
original_clone_fields = getattr(Prefix, 'clone_fields', None)
77+
try:
78+
Prefix.clone_fields = ('scope_type', 'scope_id')
79+
attrs = prefix.clone()
80+
81+
self.assertEqual(attrs['scope_type'], prefix.scope_type_id)
82+
self.assertEqual(attrs['scope_id'], prefix.scope_id)
83+
self.assertEqual(attrs['scope'], prefix.scope_id)
84+
finally:
85+
if original_clone_fields is None:
86+
delattr(Prefix, 'clone_fields')
87+
else:
88+
Prefix.clone_fields = original_clone_fields
89+
90+
def test_cloningmixin_does_not_inject_gfk_attribute_if_incomplete(self):
91+
"""
92+
Tests the cloning mixin with incomplete cloning fields does not inject the GFK attribute.
93+
94+
This test validates that the `clone` method correctly handles
95+
the case where the cloning fields are incomplete, ensuring that
96+
the generic foreign key (GFK) attribute is not injected during
97+
the cloning process.
98+
"""
99+
site = Site.objects.create(name='Test Site', slug='test-site')
100+
prefix = Prefix.objects.create(prefix='10.0.0.0/24', scope=site)
101+
102+
original_clone_fields = getattr(Prefix, 'clone_fields', None)
103+
try:
104+
Prefix.clone_fields = ('scope_type',)
105+
attrs = prefix.clone()
106+
107+
self.assertIn('scope_type', attrs)
108+
self.assertNotIn('scope', attrs)
109+
finally:
110+
if original_clone_fields is None:
111+
delattr(Prefix, 'clone_fields')
112+
else:
113+
Prefix.clone_fields = original_clone_fields

0 commit comments

Comments
 (0)