nautobot 1.6.25__py3-none-any.whl → 1.6.27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType
7
7
  from django.core.exceptions import ValidationError
8
8
  from django.core.management.base import BaseCommand, CommandError
9
9
  from django.db import models
10
+ from django.db.models.fields.json import KeyTransform
10
11
 
11
12
  from nautobot.circuits.models import Circuit
12
13
  from nautobot.dcim.models import (
@@ -376,42 +377,61 @@ def check_permissions_constraints(command):
376
377
  continue
377
378
 
378
379
  for lookup in qs._query.where.children:
379
- related_model = lookup.lhs.target.related_model or lookup.lhs.target.model
380
-
381
- if related_model in replaced_models:
382
- cls = f"{related_model.__module__}.{related_model.__name__}"
383
- warnings.append(
384
- f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
385
- f"a model ({cls}) that will be migrated to a new model by the Nautobot 2.0 migration.\n"
386
- + json.dumps(perm.constraints, indent=4)
387
- )
388
-
389
- if related_model in field_change_models:
390
- field_name = lookup.lhs.field.name
391
- if field_name in field_change_models[related_model]:
380
+ lhs = lookup.lhs
381
+
382
+ # Check if the left-hand side (lhs) of the lookup is a KeyTransform.
383
+ # KeyTransform is used for accessing nested keys in JSON fields.
384
+ if isinstance(lhs, KeyTransform):
385
+ # If lhs is a KeyTransform, it means we're dealing with a JSON field lookup usually a CustomField.
386
+ # `lhs.source_expressions` returns a list of `Col` objects representing the columns involved in the lookup.
387
+ cols = lhs.source_expressions
388
+ # - `col.field.name` gives the base field name (_custom_field_data).
389
+ # - `lhs.key_name` provides the specific custom field name (test_custom_field).
390
+ # Construct the field name by combining these with a double underscore.
391
+ suffix = f"__{lhs.key_name}"
392
+ else:
393
+ # If lhs is not a KeyTransform, it is a direct column reference.
394
+ # Wrap lhs in a list to handle it uniformly in the next step.
395
+ cols = [lhs]
396
+ suffix = ""
397
+
398
+ for col in cols:
399
+ field_name = f"{col.field.name}{suffix}"
400
+ related_model = col.target.related_model or col.target.model
401
+
402
+ if related_model in replaced_models:
392
403
  cls = f"{related_model.__module__}.{related_model.__name__}"
393
404
  warnings.append(
394
405
  f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
395
- f"a model field ({cls}.{field_name}) that may be changed by the Nautobot 2.0 migration.\n"
406
+ f"a model ({cls}) that will be migrated to a new model by the Nautobot 2.0 migration.\n"
396
407
  + json.dumps(perm.constraints, indent=4)
397
408
  )
398
409
 
399
- if lookup.lhs.field.name == "slug" and related_model is not GitRepository:
400
- warnings.append(
401
- f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
402
- f"a 'slug' field that will be deleted by the Nautobot 2.0 migration.\n"
403
- + json.dumps(perm.constraints, indent=4)
404
- )
410
+ if related_model in field_change_models:
411
+ if col.field.name in field_change_models[related_model]:
412
+ cls = f"{related_model.__module__}.{related_model.__name__}"
413
+ warnings.append(
414
+ f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
415
+ f"a model field ({cls}.{field_name}) that may be changed by the Nautobot 2.0 migration.\n"
416
+ + json.dumps(perm.constraints, indent=4)
417
+ )
405
418
 
406
- if related_model in pk_change_models:
407
- if isinstance(lookup.lhs.target, (models.fields.related.RelatedField, models.fields.UUIDField)):
408
- cls = f"{related_model.__module__}.{related_model.__name__}"
419
+ if field_name == "slug" and related_model is not GitRepository:
409
420
  warnings.append(
410
421
  f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
411
- f"an object ({cls}) that may have its primary key changed by the Nautobot 2.0 migration.\n"
422
+ f"a 'slug' field that will be deleted by the Nautobot 2.0 migration.\n"
412
423
  + json.dumps(perm.constraints, indent=4)
413
424
  )
414
425
 
426
+ if related_model in pk_change_models:
427
+ if isinstance(col.target, (models.fields.related.RelatedField, models.fields.UUIDField)):
428
+ cls = f"{related_model.__module__}.{related_model.__name__}"
429
+ warnings.append(
430
+ f"ObjectPermission '{perm}' (id: {perm.id}) has a constraint that references "
431
+ f"an object ({cls}) that may have its primary key changed by the Nautobot 2.0 migration.\n"
432
+ + json.dumps(perm.constraints, indent=4)
433
+ )
434
+
415
435
  if warnings:
416
436
  msg = """
417
437
  One or more permission constraints may be affected by the Nautobot 2.0 migration.
@@ -5,6 +5,11 @@ from django.core.management import call_command
5
5
  from django.core.management.base import CommandError
6
6
  from django.contrib.contenttypes.models import ContentType
7
7
 
8
+ from nautobot.dcim.models.devices import Device
9
+ from nautobot.dcim.models.sites import Site
10
+ from nautobot.extras.choices import CustomFieldTypeChoices
11
+ from nautobot.extras.models.customfields import CustomField
12
+ from nautobot.users.models import ObjectPermission
8
13
  from nautobot.utilities.testing import TestCase
9
14
  from nautobot.dcim.models import VirtualChassis
10
15
  from nautobot.extras.datasources.registry import get_datasource_contents
@@ -25,6 +30,17 @@ class PreMigrateCommandTest(TestCase):
25
30
  )
26
31
  self.git_repo.save(trigger_resync=False)
27
32
 
33
+ # Adding this test data to assert that https://github.com/nautobot/nautobot/issues/6081 is fixed
34
+ ct = ContentType.objects.get_for_model(GitRepository)
35
+ custom_field = CustomField.objects.create(type=CustomFieldTypeChoices.TYPE_TEXT, name="test_custom_field")
36
+ custom_field.content_types.add(ct)
37
+ self.obj_perm = ObjectPermission.objects.create(
38
+ name="Test permission",
39
+ constraints={"_custom_field_data__test_custom_field__in": ["test-1", "test-2"]},
40
+ actions=["view"],
41
+ )
42
+ self.obj_perm.object_types.add(ct)
43
+
28
44
  def run_command(self, *args):
29
45
  out = StringIO()
30
46
  err = StringIO()
@@ -42,6 +58,8 @@ class PreMigrateCommandTest(TestCase):
42
58
  out, err = self.run_command()
43
59
 
44
60
  self.assertIn("All pre-migration checks passed.", out)
61
+ # Assert Permission constrain warning not logged
62
+ self.assertNotIn(f"ObjectPermission '{self.obj_perm.name}'", out)
45
63
  self.assertEqual("", err)
46
64
 
47
65
  def test_configcontext_failure(self):
@@ -80,3 +98,22 @@ class PreMigrateCommandTest(TestCase):
80
98
 
81
99
  with self.assertRaises(CommandError):
82
100
  self.run_command()
101
+
102
+ def test_permission_constraints_failure(self):
103
+ """Test permission constraints logs warning when needed for CustomField."""
104
+ device_ct = ContentType.objects.get_for_model(Device)
105
+ site_ct = ContentType.objects.get_for_model(Site)
106
+ custom_field = CustomField.objects.create(type=CustomFieldTypeChoices.TYPE_TEXT, name="custom_field")
107
+ custom_field.content_types.add(site_ct)
108
+ obj_perm = ObjectPermission.objects.create(
109
+ name="Test permission 2",
110
+ constraints={"site___custom_field_data__custom_field__in": ["test-1", "test-2"]},
111
+ actions=["view"],
112
+ )
113
+ obj_perm.object_types.add(device_ct)
114
+
115
+ out, _ = self.run_command()
116
+ self.assertIn(
117
+ f"ObjectPermission 'Test permission 2' (id: {obj_perm.pk}) has a constraint that references a model (nautobot.dcim.models.sites.Site) that will be migrated to a new model by the Nautobot 2.0 migration.",
118
+ out,
119
+ )
@@ -842,9 +842,16 @@ class Device(PrimaryModel, ConfigContextModel, StatusModel):
842
842
  # Update Site and Rack assignment for any child Devices
843
843
  devices = Device.objects.filter(parent_bay__device=self)
844
844
  for device in devices:
845
- device.site = self.site
846
- device.rack = self.rack
847
- device.save()
845
+ save_child_device = False
846
+ if device.site != self.site:
847
+ device.site = self.site
848
+ save_child_device = True
849
+ if device.rack != self.rack:
850
+ device.rack = self.rack
851
+ save_child_device = True
852
+
853
+ if save_child_device:
854
+ device.save()
848
855
 
849
856
  def create_components(self):
850
857
  """Create device components from the device type definition."""
@@ -14,6 +14,7 @@ from nautobot.dcim.choices import (
14
14
  InterfaceTypeChoices,
15
15
  PortTypeChoices,
16
16
  PowerOutletFeedLegChoices,
17
+ SubdeviceRoleChoices,
17
18
  )
18
19
  from nautobot.dcim.models import (
19
20
  Cable,
@@ -925,6 +926,14 @@ class DeviceTestCase(TestCase):
925
926
  manufacturer=manufacturer,
926
927
  model="Test Device Type 1",
927
928
  slug="test-device-type-1",
929
+ subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
930
+ )
931
+ self.child_devicetype = DeviceType.objects.create(
932
+ model="Child Device Type 1",
933
+ slug="child-device-type-1",
934
+ manufacturer=manufacturer,
935
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
936
+ u_height=0,
928
937
  )
929
938
  self.device_role = DeviceRole.objects.create(
930
939
  name="Test Device Role 1", slug="test-device-role-1", color="ff0000"
@@ -1164,6 +1173,71 @@ class DeviceTestCase(TestCase):
1164
1173
  ):
1165
1174
  d1.validated_save()
1166
1175
 
1176
+ def test_child_devices_are_not_saved_when_unnecessary(self):
1177
+ parent_device = Device.objects.create(
1178
+ name="Parent Device 1",
1179
+ site=self.site,
1180
+ device_type=self.device_type,
1181
+ device_role=self.device_role,
1182
+ status=self.device_status,
1183
+ )
1184
+ parent_device.validated_save()
1185
+
1186
+ child_device = Device.objects.create(
1187
+ name="Child Device 1",
1188
+ site=parent_device.site,
1189
+ device_type=self.child_devicetype,
1190
+ device_role=parent_device.device_role,
1191
+ status=self.device_status,
1192
+ )
1193
+ child_device.validated_save()
1194
+ child_mtime_before_parent_saved = str(child_device.last_updated)
1195
+
1196
+ devicebay = DeviceBay.objects.get(device=parent_device, name="Device Bay 1")
1197
+ devicebay.installed_device = child_device
1198
+ devicebay.validated_save()
1199
+
1200
+ #
1201
+ # Tests
1202
+ #
1203
+
1204
+ #
1205
+ # On a NOOP save, the child device shouldn't be updated
1206
+ parent_device.save()
1207
+
1208
+ child_mtime_after_parent_noop_save = str(Device.objects.get(name="Child Device 1").last_updated)
1209
+
1210
+ self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_noop_save)
1211
+
1212
+ #
1213
+ # On a serial number update, the child device shouldn't be updated
1214
+ parent_device.serial = "12345"
1215
+ parent_device.save()
1216
+
1217
+ child_mtime_after_parent_serial_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1218
+
1219
+ self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_serial_update_save)
1220
+
1221
+ #
1222
+ # If the parent rack updates, the child mtime should update.
1223
+ rack = Rack.objects.create(name="Rack 1", site=parent_device.site)
1224
+ parent_device.rack = rack
1225
+ parent_device.save()
1226
+
1227
+ child_mtime_after_parent_rack_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1228
+
1229
+ self.assertNotEqual(child_mtime_after_parent_noop_save, child_mtime_after_parent_rack_update_save)
1230
+
1231
+ #
1232
+ # If the parent site updates, the child mtime should update
1233
+ site = Site.objects.create(name="New Site 1")
1234
+ parent_device.site = site
1235
+ parent_device.save()
1236
+
1237
+ child_mtime_after_parent_site_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1238
+
1239
+ self.assertNotEqual(child_mtime_after_parent_rack_update_save, child_mtime_after_parent_site_update_save)
1240
+
1167
1241
 
1168
1242
  class CableTestCase(TestCase):
1169
1243
  def setUp(self):