Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
d9b8caef5f Closes #21303: Cache serialized post-change data on object 2026-01-29 16:38:23 -05:00
6 changed files with 43 additions and 189 deletions

View File

@@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
from jsonschema.exceptions import ValidationError as JSONValidationError
from dcim.choices import *
from dcim.utils import create_port_mappings, update_interface_bridges
from dcim.utils import update_interface_bridges
from extras.models import ConfigContextModel, CustomField
from netbox.models import PrimaryModel
from netbox.models.features import ImageAttachmentsMixin
@@ -361,7 +361,5 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
update_fields=update_fields
)
# Replicate any front/rear port mappings from the ModuleType
create_port_mappings(self.device, self.module_type, self)
# Interface bridges have to be set after interface instantiation
update_interface_bridges(self.device, self.module_type.interfacetemplates, self)

View File

@@ -875,142 +875,6 @@ class ModuleBayTestCase(TestCase):
self.assertIsNone(bay2.parent)
self.assertIsNone(bay2.module)
def test_module_installation_creates_port_mappings(self):
"""
Test that installing a module with front/rear port templates correctly
creates PortMapping instances for the device.
"""
device = Device.objects.first()
manufacturer = Manufacturer.objects.first()
module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 1')
# Create a module type with a rear port template
module_type_with_mappings = ModuleType.objects.create(
manufacturer=manufacturer,
model='Module Type With Mappings',
)
# Create a rear port template with 12 positions (splice)
rear_port_template = RearPortTemplate.objects.create(
module_type=module_type_with_mappings,
name='Rear Port 1',
type=PortTypeChoices.TYPE_SPLICE,
positions=12,
)
# Create 12 front port templates mapped to the rear port
front_port_templates = []
for i in range(1, 13):
front_port_template = FrontPortTemplate.objects.create(
module_type=module_type_with_mappings,
name=f'port {i}',
type=PortTypeChoices.TYPE_LC,
positions=1,
)
front_port_templates.append(front_port_template)
# Create port template mapping
PortTemplateMapping.objects.create(
device_type=None,
module_type=module_type_with_mappings,
front_port=front_port_template,
front_port_position=1,
rear_port=rear_port_template,
rear_port_position=i,
)
# Install the module
module = Module.objects.create(
device=device,
module_bay=module_bay,
module_type=module_type_with_mappings,
status=ModuleStatusChoices.STATUS_ACTIVE,
)
# Verify that front ports were created
front_ports = FrontPort.objects.filter(device=device, module=module)
self.assertEqual(front_ports.count(), 12)
# Verify that the rear port was created
rear_ports = RearPort.objects.filter(device=device, module=module)
self.assertEqual(rear_ports.count(), 1)
rear_port = rear_ports.first()
self.assertEqual(rear_port.positions, 12)
# Verify that port mappings were created
port_mappings = PortMapping.objects.filter(front_port__module=module)
self.assertEqual(port_mappings.count(), 12)
# Verify each mapping is correct
for i, front_port_template in enumerate(front_port_templates, start=1):
front_port = FrontPort.objects.get(
device=device,
name=front_port_template.name,
module=module,
)
# Check that a mapping exists for this front port
mapping = PortMapping.objects.get(
device=device,
front_port=front_port,
front_port_position=1,
)
self.assertEqual(mapping.rear_port, rear_port)
self.assertEqual(mapping.front_port_position, 1)
self.assertEqual(mapping.rear_port_position, i)
def test_module_installation_without_mappings(self):
"""
Test that installing a module without port template mappings
doesn't create any PortMapping instances.
"""
device = Device.objects.first()
manufacturer = Manufacturer.objects.first()
module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 2')
# Create a module type without any port template mappings
module_type_no_mappings = ModuleType.objects.create(
manufacturer=manufacturer,
model='Module Type Without Mappings',
)
# Create a rear port template
RearPortTemplate.objects.create(
module_type=module_type_no_mappings,
name='Rear Port 1',
type=PortTypeChoices.TYPE_SPLICE,
positions=12,
)
# Create front port templates but DO NOT create PortTemplateMapping rows
for i in range(1, 13):
FrontPortTemplate.objects.create(
module_type=module_type_no_mappings,
name=f'port {i}',
type=PortTypeChoices.TYPE_LC,
positions=1,
)
# Install the module
module = Module.objects.create(
device=device,
module_bay=module_bay,
module_type=module_type_no_mappings,
status=ModuleStatusChoices.STATUS_ACTIVE,
)
# Verify no port mappings were created for this module
port_mappings = PortMapping.objects.filter(
device=device,
front_port__module=module,
front_port_position=1,
)
self.assertEqual(port_mappings.count(), 0)
self.assertEqual(FrontPort.objects.filter(module=module).count(), 12)
self.assertEqual(RearPort.objects.filter(module=module).count(), 1)
self.assertEqual(PortMapping.objects.filter(front_port__module=module).count(), 0)
class CableTestCase(TestCase):

View File

@@ -85,13 +85,13 @@ def update_interface_bridges(device, interface_templates, module=None):
interface.save()
def create_port_mappings(device, device_or_module_type, module=None):
def create_port_mappings(device, device_type, module=None):
"""
Replicate all front/rear port mappings from a DeviceType or ModuleType to the given device.
Replicate all front/rear port mappings from a DeviceType to the given device.
"""
from dcim.models import FrontPort, PortMapping, RearPort
templates = device_or_module_type.port_mappings.prefetch_related('front_port', 'rear_port')
templates = device_type.port_mappings.prefetch_related('front_port', 'rear_port')
# Cache front & rear ports for efficient lookups by name
front_ports = {

View File

@@ -53,9 +53,9 @@ def serialize_for_event(instance):
def get_snapshots(instance, event_type):
snapshots = {
'prechange': getattr(instance, '_prechange_snapshot', None),
'postchange': None,
'postchange': getattr(instance, '_postchange_snapshot', None),
}
if event_type != OBJECT_DELETED:
if snapshots['postchange'] is None and event_type != OBJECT_DELETED:
# Use model's serialize_object() method if defined; fall back to serialize_object() utility function
if hasattr(instance, 'serialize_object'):
snapshots['postchange'] = instance.serialize_object()

View File

@@ -121,7 +121,8 @@ class ChangeLoggingMixin(DeleteMixin, models.Model):
if hasattr(self, '_prechange_snapshot'):
objectchange.prechange_data = self._prechange_snapshot
if action in (ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE):
objectchange.postchange_data = self.serialize_object(exclude=exclude)
self._postchange_snapshot = self.serialize_object(exclude=exclude)
objectchange.postchange_data = self._postchange_snapshot
return objectchange

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-30 05:18+0000\n"
"POT-Creation-Date: 2026-01-29 05:16+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -293,8 +293,8 @@ msgstr ""
#: netbox/circuits/filtersets.py:278 netbox/circuits/filtersets.py:382
#: netbox/circuits/filtersets.py:547 netbox/core/filtersets.py:85
#: netbox/core/filtersets.py:154 netbox/core/filtersets.py:180
#: netbox/core/filtersets.py:220 netbox/dcim/filtersets.py:810
#: netbox/core/filtersets.py:150 netbox/core/filtersets.py:176
#: netbox/core/filtersets.py:216 netbox/dcim/filtersets.py:810
#: netbox/dcim/filtersets.py:1568 netbox/dcim/filtersets.py:2692
#: netbox/extras/filtersets.py:48 netbox/extras/filtersets.py:71
#: netbox/extras/filtersets.py:101 netbox/extras/filtersets.py:142
@@ -763,7 +763,7 @@ msgstr ""
#: netbox/circuits/forms/filtersets.py:132
#: netbox/circuits/forms/filtersets.py:322
#: netbox/circuits/forms/filtersets.py:338 netbox/core/forms/filtersets.py:75
#: netbox/core/forms/filtersets.py:147 netbox/dcim/forms/bulk_edit.py:818
#: netbox/core/forms/filtersets.py:143 netbox/dcim/forms/bulk_edit.py:818
#: netbox/dcim/forms/bulk_import.py:480 netbox/dcim/forms/filtersets.py:199
#: netbox/dcim/forms/filtersets.py:232 netbox/dcim/forms/filtersets.py:1012
#: netbox/dcim/forms/filtersets.py:1155 netbox/dcim/forms/filtersets.py:1285
@@ -2189,7 +2189,7 @@ msgstr ""
msgid "Data source (name)"
msgstr ""
#: netbox/core/filtersets.py:190 netbox/dcim/filtersets.py:521
#: netbox/core/filtersets.py:186 netbox/dcim/filtersets.py:521
#: netbox/extras/filtersets.py:302 netbox/extras/filtersets.py:355
#: netbox/extras/filtersets.py:401 netbox/extras/filtersets.py:424
#: netbox/extras/filtersets.py:490 netbox/users/filtersets.py:31
@@ -2197,7 +2197,7 @@ msgstr ""
msgid "User (ID)"
msgstr ""
#: netbox/core/filtersets.py:196
#: netbox/core/filtersets.py:192
msgid "User name"
msgstr ""
@@ -2277,7 +2277,7 @@ msgstr ""
msgid "Creation"
msgstr ""
#: netbox/core/forms/filtersets.py:82 netbox/core/forms/filtersets.py:172
#: netbox/core/forms/filtersets.py:82 netbox/core/forms/filtersets.py:168
#: netbox/extras/forms/filtersets.py:577 netbox/extras/tables/tables.py:271
#: netbox/extras/tables/tables.py:338 netbox/extras/tables/tables.py:364
#: netbox/extras/tables/tables.py:383 netbox/extras/tables/tables.py:415
@@ -2288,44 +2288,39 @@ msgstr ""
msgid "Object Type"
msgstr ""
#: netbox/core/forms/filtersets.py:92 netbox/core/tables/jobs.py:46
#: netbox/templates/core/job.html:63 netbox/templates/core/rq_task.html:61
msgid "Queue"
msgstr ""
#: netbox/core/forms/filtersets.py:96
#: netbox/core/forms/filtersets.py:92
msgid "Created after"
msgstr ""
#: netbox/core/forms/filtersets.py:101
#: netbox/core/forms/filtersets.py:97
msgid "Created before"
msgstr ""
#: netbox/core/forms/filtersets.py:106
#: netbox/core/forms/filtersets.py:102
msgid "Scheduled after"
msgstr ""
#: netbox/core/forms/filtersets.py:111
#: netbox/core/forms/filtersets.py:107
msgid "Scheduled before"
msgstr ""
#: netbox/core/forms/filtersets.py:116
#: netbox/core/forms/filtersets.py:112
msgid "Started after"
msgstr ""
#: netbox/core/forms/filtersets.py:121
#: netbox/core/forms/filtersets.py:117
msgid "Started before"
msgstr ""
#: netbox/core/forms/filtersets.py:126
#: netbox/core/forms/filtersets.py:122
msgid "Completed after"
msgstr ""
#: netbox/core/forms/filtersets.py:131
#: netbox/core/forms/filtersets.py:127
msgid "Completed before"
msgstr ""
#: netbox/core/forms/filtersets.py:138 netbox/core/forms/filtersets.py:167
#: netbox/core/forms/filtersets.py:134 netbox/core/forms/filtersets.py:163
#: netbox/dcim/forms/bulk_edit.py:455 netbox/dcim/forms/filtersets.py:509
#: netbox/dcim/forms/model_forms.py:326 netbox/extras/forms/filtersets.py:572
#: netbox/extras/forms/filtersets.py:592 netbox/extras/tables/tables.py:391
@@ -2341,22 +2336,22 @@ msgstr ""
msgid "User"
msgstr ""
#: netbox/core/forms/filtersets.py:146 netbox/core/tables/change_logging.py:15
#: netbox/core/tables/jobs.py:72 netbox/extras/tables/tables.py:773
#: netbox/core/forms/filtersets.py:142 netbox/core/tables/change_logging.py:15
#: netbox/core/tables/jobs.py:69 netbox/extras/tables/tables.py:773
#: netbox/extras/tables/tables.py:828
#: netbox/templates/core/objectchange.html:32
msgid "Time"
msgstr ""
#: netbox/core/forms/filtersets.py:151 netbox/extras/forms/filtersets.py:561
#: netbox/core/forms/filtersets.py:147 netbox/extras/forms/filtersets.py:561
msgid "After"
msgstr ""
#: netbox/core/forms/filtersets.py:156 netbox/extras/forms/filtersets.py:566
#: netbox/core/forms/filtersets.py:152 netbox/extras/forms/filtersets.py:566
msgid "Before"
msgstr ""
#: netbox/core/forms/filtersets.py:160 netbox/core/tables/change_logging.py:29
#: netbox/core/forms/filtersets.py:156 netbox/core/tables/change_logging.py:29
#: netbox/extras/forms/model_forms.py:484
#: netbox/templates/core/objectchange.html:46
#: netbox/templates/extras/eventrule.html:71
@@ -2726,36 +2721,28 @@ msgid "job ID"
msgstr ""
#: netbox/core/models/jobs.py:116
msgid "queue name"
msgstr ""
#: netbox/core/models/jobs.py:119
msgid "Name of the queue in which this job was enqueued"
msgstr ""
#: netbox/core/models/jobs.py:122
msgid "log entries"
msgstr ""
#: netbox/core/models/jobs.py:138
#: netbox/core/models/jobs.py:132
msgid "job"
msgstr ""
#: netbox/core/models/jobs.py:139
#: netbox/core/models/jobs.py:133
msgid "jobs"
msgstr ""
#: netbox/core/models/jobs.py:169
#: netbox/core/models/jobs.py:163
#, python-brace-format
msgid "Jobs cannot be assigned to this object type ({type})."
msgstr ""
#: netbox/core/models/jobs.py:226
#: netbox/core/models/jobs.py:216
#, python-brace-format
msgid "Invalid status for job termination. Choices are: {choices}"
msgstr ""
#: netbox/core/models/jobs.py:283
#: netbox/core/models/jobs.py:273
msgid ""
"enqueue() cannot be called with values for both schedule_at and immediate."
msgstr ""
@@ -2801,7 +2788,7 @@ msgstr ""
msgid "Request ID"
msgstr ""
#: netbox/core/tables/change_logging.py:45 netbox/core/tables/jobs.py:79
#: netbox/core/tables/change_logging.py:45 netbox/core/tables/jobs.py:76
#: netbox/extras/tables/tables.py:784 netbox/extras/tables/tables.py:841
#: netbox/templates/core/objectchange.html:68
msgid "Message"
@@ -2844,16 +2831,16 @@ msgstr ""
msgid "Interval"
msgstr ""
#: netbox/core/tables/jobs.py:49
#: netbox/core/tables/jobs.py:46
msgid "Log Entries"
msgstr ""
#: netbox/core/tables/jobs.py:76 netbox/extras/tables/tables.py:778
#: netbox/core/tables/jobs.py:73 netbox/extras/tables/tables.py:778
#: netbox/extras/tables/tables.py:832
msgid "Level"
msgstr ""
#: netbox/core/tables/jobs.py:83
#: netbox/core/tables/jobs.py:80
msgid "No log entries"
msgstr ""
@@ -8886,7 +8873,7 @@ msgstr ""
#: netbox/extras/forms/filtersets.py:176 netbox/extras/forms/filtersets.py:377
#: netbox/extras/forms/filtersets.py:400 netbox/extras/forms/filtersets.py:496
#: netbox/extras/forms/model_forms.py:690 netbox/templates/core/job.html:73
#: netbox/extras/forms/model_forms.py:690 netbox/templates/core/job.html:69
#: netbox/templates/extras/eventrule.html:84
msgid "Data"
msgstr ""
@@ -13587,6 +13574,10 @@ msgstr ""
msgid "Enqueue"
msgstr ""
#: netbox/templates/core/rq_task.html:61
msgid "Queue"
msgstr ""
#: netbox/templates/core/rq_task.html:65
msgid "Timeout"
msgstr ""