nautobot 2.3.15__py3-none-any.whl → 2.3.16__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.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (106) hide show
  1. nautobot/circuits/views.py +3 -3
  2. nautobot/cloud/models.py +1 -1
  3. nautobot/core/api/fields.py +5 -5
  4. nautobot/core/api/serializers.py +9 -9
  5. nautobot/core/api/views.py +3 -2
  6. nautobot/core/apps/__init__.py +5 -2
  7. nautobot/core/celery/schedulers.py +1 -1
  8. nautobot/core/filters.py +19 -16
  9. nautobot/core/forms/fields.py +5 -5
  10. nautobot/core/graphql/types.py +1 -1
  11. nautobot/core/jobs/__init__.py +4 -4
  12. nautobot/core/jobs/cleanup.py +1 -1
  13. nautobot/core/jobs/groups.py +1 -1
  14. nautobot/core/management/commands/validate_models.py +1 -1
  15. nautobot/core/models/__init__.py +1 -1
  16. nautobot/core/models/query_functions.py +2 -2
  17. nautobot/core/models/tree_queries.py +2 -2
  18. nautobot/core/tables.py +5 -5
  19. nautobot/core/testing/filters.py +7 -3
  20. nautobot/core/testing/views.py +5 -0
  21. nautobot/core/tests/runner.py +1 -1
  22. nautobot/core/views/generic.py +51 -43
  23. nautobot/core/views/mixins.py +21 -11
  24. nautobot/dcim/api/serializers.py +48 -48
  25. nautobot/dcim/forms.py +2 -0
  26. nautobot/dcim/graphql/types.py +2 -2
  27. nautobot/dcim/models/device_component_templates.py +2 -2
  28. nautobot/dcim/models/device_components.py +22 -20
  29. nautobot/dcim/models/devices.py +1 -1
  30. nautobot/dcim/models/locations.py +3 -3
  31. nautobot/dcim/models/power.py +6 -5
  32. nautobot/dcim/models/racks.py +4 -4
  33. nautobot/dcim/tables/__init__.py +3 -3
  34. nautobot/dcim/tables/devicetypes.py +2 -2
  35. nautobot/dcim/tests/test_filters.py +1 -0
  36. nautobot/dcim/tests/test_graphql.py +52 -0
  37. nautobot/dcim/tests/test_models.py +4 -1
  38. nautobot/dcim/views.py +1 -1
  39. nautobot/extras/api/customfields.py +2 -2
  40. nautobot/extras/api/serializers.py +72 -69
  41. nautobot/extras/api/views.py +4 -4
  42. nautobot/extras/health_checks.py +1 -2
  43. nautobot/extras/jobs.py +5 -5
  44. nautobot/extras/managers.py +3 -1
  45. nautobot/extras/migrations/0018_joblog_data_migration.py +7 -9
  46. nautobot/extras/models/groups.py +13 -9
  47. nautobot/extras/models/jobs.py +4 -4
  48. nautobot/extras/models/models.py +2 -2
  49. nautobot/extras/plugins/views.py +1 -1
  50. nautobot/extras/tables.py +5 -5
  51. nautobot/extras/test_jobs/api_test_job.py +1 -1
  52. nautobot/extras/test_jobs/atomic_transaction.py +2 -2
  53. nautobot/extras/test_jobs/dry_run.py +1 -1
  54. nautobot/extras/test_jobs/fail.py +5 -5
  55. nautobot/extras/test_jobs/file_output.py +1 -1
  56. nautobot/extras/test_jobs/file_upload_fail.py +1 -1
  57. nautobot/extras/test_jobs/file_upload_pass.py +1 -1
  58. nautobot/extras/test_jobs/ipaddress_vars.py +3 -1
  59. nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +1 -1
  60. nautobot/extras/test_jobs/location_with_custom_field.py +1 -1
  61. nautobot/extras/test_jobs/log_redaction.py +1 -1
  62. nautobot/extras/test_jobs/log_skip_db_logging.py +1 -1
  63. nautobot/extras/test_jobs/modify_db.py +1 -1
  64. nautobot/extras/test_jobs/object_var_optional.py +1 -1
  65. nautobot/extras/test_jobs/object_var_required.py +1 -1
  66. nautobot/extras/test_jobs/object_vars.py +1 -1
  67. nautobot/extras/test_jobs/pass.py +3 -3
  68. nautobot/extras/test_jobs/profiling.py +1 -1
  69. nautobot/extras/test_jobs/relative_import.py +3 -3
  70. nautobot/extras/test_jobs/soft_time_limit_greater_than_time_limit.py +1 -1
  71. nautobot/extras/test_jobs/task_queues.py +1 -1
  72. nautobot/extras/tests/test_api.py +13 -13
  73. nautobot/extras/tests/test_customfields.py +1 -1
  74. nautobot/extras/tests/test_datasources.py +2 -1
  75. nautobot/extras/tests/test_dynamicgroups.py +1 -1
  76. nautobot/extras/tests/test_filters.py +6 -6
  77. nautobot/extras/tests/test_jobs.py +11 -11
  78. nautobot/extras/tests/test_models.py +10 -10
  79. nautobot/extras/tests/test_relationships.py +1 -1
  80. nautobot/extras/tests/test_views.py +16 -16
  81. nautobot/extras/views.py +20 -16
  82. nautobot/ipam/api/fields.py +3 -3
  83. nautobot/ipam/api/serializers.py +33 -33
  84. nautobot/ipam/api/views.py +37 -61
  85. nautobot/ipam/querysets.py +2 -2
  86. nautobot/ipam/tests/test_api.py +12 -1
  87. nautobot/ipam/tests/test_forms.py +51 -47
  88. nautobot/ipam/tests/test_migrations.py +30 -30
  89. nautobot/ipam/tests/test_querysets.py +14 -0
  90. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
  91. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1 -1
  92. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2 -2
  93. nautobot/project-static/docs/release-notes/version-2.3.html +181 -99
  94. nautobot/project-static/docs/search/search_index.json +1 -1
  95. nautobot/project-static/docs/sitemap.xml +270 -270
  96. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  97. nautobot/users/admin.py +1 -1
  98. nautobot/users/api/serializers.py +4 -4
  99. nautobot/users/api/views.py +1 -1
  100. nautobot/virtualization/api/serializers.py +4 -4
  101. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/METADATA +1 -1
  102. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/RECORD +106 -106
  103. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/WHEEL +1 -1
  104. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/LICENSE.txt +0 -0
  105. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/NOTICE +0 -0
  106. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,8 @@
1
1
  """Test IPAM forms."""
2
2
 
3
+ from __future__ import annotations # python 3.8
4
+
5
+ from django.forms import Form
3
6
  from django.test import TestCase
4
7
 
5
8
  from nautobot.core.testing.forms import FormTestCases
@@ -9,62 +12,63 @@ from nautobot.ipam.choices import IPAddressTypeChoices
9
12
  from nautobot.ipam.models import IPAddress, Namespace, Prefix
10
13
 
11
14
 
12
- class BaseNetworkFormTest:
13
- form_class = None
14
- field_name = None
15
- object_name = None
16
- extra_data = {}
17
-
18
- def setUp(self):
19
- super().setUp()
20
- self.namespace = Namespace.objects.create(name="IPAM Form Test")
21
- self.status = Status.objects.get(name="Active")
22
- self.prefix_status = Status.objects.get_for_model(Prefix).first()
23
- self.ip_status = Status.objects.get_for_model(IPAddress).first()
24
- self.parent = Prefix.objects.create(
25
- prefix="192.168.1.0/24", namespace=self.namespace, status=self.prefix_status
26
- )
27
- self.parent2 = Prefix.objects.create(
28
- prefix="192.168.0.0/16", namespace=self.namespace, status=self.prefix_status
29
- )
30
- self.parent6 = Prefix.objects.create(
31
- prefix="2001:0db8::/40", namespace=self.namespace, status=self.prefix_status
32
- )
15
+ class NetworkFormTestCases:
16
+ class BaseNetworkFormTest(TestCase):
17
+ form_class: type[Form]
18
+ field_name: str
19
+ object_name: str
20
+ extra_data = {}
21
+
22
+ def setUp(self):
23
+ super().setUp()
24
+ self.namespace = Namespace.objects.create(name="IPAM Form Test")
25
+ self.status = Status.objects.get(name="Active")
26
+ self.prefix_status = Status.objects.get_for_model(Prefix).first()
27
+ self.ip_status = Status.objects.get_for_model(IPAddress).first()
28
+ self.parent = Prefix.objects.create(
29
+ prefix="192.168.1.0/24", namespace=self.namespace, status=self.prefix_status
30
+ )
31
+ self.parent2 = Prefix.objects.create(
32
+ prefix="192.168.0.0/16", namespace=self.namespace, status=self.prefix_status
33
+ )
34
+ self.parent6 = Prefix.objects.create(
35
+ prefix="2001:0db8::/40", namespace=self.namespace, status=self.prefix_status
36
+ )
33
37
 
34
- def test_valid_ip_address(self):
35
- data = {self.field_name: "192.168.2.0/24", "namespace": self.namespace, "status": self.status}
36
- data.update(self.extra_data)
37
- form = self.form_class(data)
38
+ def test_valid_ip_address(self):
39
+ data = {self.field_name: "192.168.2.0/24", "namespace": self.namespace, "status": self.status}
40
+ data.update(self.extra_data)
41
+ form = self.form_class(data)
38
42
 
39
- self.assertTrue(form.is_valid())
40
- self.assertTrue(form.save())
43
+ self.assertTrue(form.is_valid())
44
+ self.assertTrue(form.save())
41
45
 
42
- def test_address_invalid_ipv4(self):
43
- data = {self.field_name: "192.168.0.1/64", "namespace": self.namespace, "status": self.status}
44
- data.update(self.extra_data)
45
- form = self.form_class(data)
46
+ def test_address_invalid_ipv4(self):
47
+ data = {self.field_name: "192.168.0.1/64", "namespace": self.namespace, "status": self.status}
48
+ data.update(self.extra_data)
49
+ form = self.form_class(data)
46
50
 
47
- self.assertFalse(form.is_valid())
48
- self.assertEqual("Please specify a valid IPv4 or IPv6 address.", form.errors[self.field_name][0])
51
+ self.assertFalse(form.is_valid())
52
+ self.assertEqual("Please specify a valid IPv4 or IPv6 address.", form.errors[self.field_name][0])
49
53
 
50
- def test_address_zero_mask(self):
51
- data = {self.field_name: "192.168.0.1/0", "namespace": self.namespace, "status": self.status}
52
- data.update(self.extra_data)
53
- form = self.form_class(data)
54
+ def test_address_zero_mask(self):
55
+ data = {self.field_name: "192.168.0.1/0", "namespace": self.namespace, "status": self.status}
56
+ data.update(self.extra_data)
57
+ form = self.form_class(data)
54
58
 
55
- # With the advent of `Prefix.parent`, it's now possible to create a /0 .
56
- self.assertTrue(form.is_valid())
59
+ # With the advent of `Prefix.parent`, it's now possible to create a /0 .
60
+ self.assertTrue(form.is_valid())
57
61
 
58
- def test_address_missing_mask(self):
59
- data = {self.field_name: "192.168.0.1", "namespace": self.namespace, "status": self.status}
60
- data.update(self.extra_data)
61
- form = self.form_class(data)
62
+ def test_address_missing_mask(self):
63
+ data = {self.field_name: "192.168.0.1", "namespace": self.namespace, "status": self.status}
64
+ data.update(self.extra_data)
65
+ form = self.form_class(data)
62
66
 
63
- self.assertFalse(form.is_valid())
64
- self.assertEqual("CIDR mask (e.g. /24) is required.", form.errors[self.field_name][0])
67
+ self.assertFalse(form.is_valid())
68
+ self.assertEqual("CIDR mask (e.g. /24) is required.", form.errors[self.field_name][0])
65
69
 
66
70
 
67
- class PrefixFormTest(BaseNetworkFormTest, FormTestCases.BaseFormTestCase):
71
+ class PrefixFormTest(NetworkFormTestCases.BaseNetworkFormTest, FormTestCases.BaseFormTestCase):
68
72
  form_class = forms.PrefixForm
69
73
  field_name = "prefix"
70
74
  object_name = "prefix"
@@ -79,7 +83,7 @@ class PrefixFormTest(BaseNetworkFormTest, FormTestCases.BaseFormTestCase):
79
83
  }
80
84
 
81
85
 
82
- class IPAddressFormTest(BaseNetworkFormTest, TestCase):
86
+ class IPAddressFormTest(NetworkFormTestCases.BaseNetworkFormTest):
83
87
  form_class = forms.IPAddressForm
84
88
  field_name = "address"
85
89
  object_name = "IP address"
@@ -29,29 +29,29 @@ class AggregateToPrefixMigrationTestCase(NautobotDataMigrationTest):
29
29
  request_id=uuid.uuid4(),
30
30
  )
31
31
 
32
- def populateDataBeforeMigration(self, apps):
32
+ def populateDataBeforeMigration(self, installed_apps):
33
33
  """Populate Aggregate data before migrating to Prefixes"""
34
34
 
35
- self.aggregate = apps.get_model("ipam", "Aggregate")
35
+ self.aggregate = installed_apps.get_model("ipam", "Aggregate")
36
36
  # Workaround for django-taggit manager not working in migrations.
37
37
  # https://github.com/jazzband/django-taggit/issues/101
38
38
  # https://github.com/jazzband/django-taggit/issues/454
39
39
  self.aggregate.tags = TagsField()
40
- self.computed_field = apps.get_model("extras", "computedfield")
41
- self.content_type = apps.get_model("contenttypes", "ContentType")
42
- self.custom_field = apps.get_model("extras", "customfield")
43
- self.custom_link = apps.get_model("extras", "customlink")
44
- self.dynamic_group = apps.get_model("extras", "DynamicGroup")
45
- self.note = apps.get_model("extras", "note")
46
- self.object_change = apps.get_model("extras", "objectchange")
47
- self.object_permission = apps.get_model("users", "objectpermission")
48
- self.prefix = apps.get_model("ipam", "prefix")
40
+ self.computed_field = installed_apps.get_model("extras", "computedfield")
41
+ self.content_type = installed_apps.get_model("contenttypes", "ContentType")
42
+ self.custom_field = installed_apps.get_model("extras", "customfield")
43
+ self.custom_link = installed_apps.get_model("extras", "customlink")
44
+ self.dynamic_group = installed_apps.get_model("extras", "DynamicGroup")
45
+ self.note = installed_apps.get_model("extras", "note")
46
+ self.object_change = installed_apps.get_model("extras", "objectchange")
47
+ self.object_permission = installed_apps.get_model("users", "objectpermission")
48
+ self.prefix = installed_apps.get_model("ipam", "prefix")
49
49
  self.prefix.tags = TagsField()
50
- self.relationship = apps.get_model("extras", "relationship")
51
- self.relationship_association = apps.get_model("extras", "relationshipassociation")
52
- self.rir = apps.get_model("ipam", "RIR")
53
- self.status = apps.get_model("extras", "status")
54
- self.tag = apps.get_model("extras", "tag")
50
+ self.relationship = installed_apps.get_model("extras", "relationship")
51
+ self.relationship_association = installed_apps.get_model("extras", "relationshipassociation")
52
+ self.rir = installed_apps.get_model("ipam", "RIR")
53
+ self.status = installed_apps.get_model("extras", "status")
54
+ self.tag = installed_apps.get_model("extras", "tag")
55
55
 
56
56
  self.aggregate_ct = self.content_type.objects.get_for_model(self.aggregate)
57
57
  self.prefix_ct = self.content_type.objects.get_for_model(self.prefix)
@@ -114,8 +114,8 @@ class AggregateToPrefixMigrationTestCase(NautobotDataMigrationTest):
114
114
  self.aggregate1.tags.add("AggregateTagA", "AggregateTagB")
115
115
  self.aggregate2.tags.add("AggregateTagA")
116
116
  self.aggregate3.tags.add("AggregateTagB")
117
- self.aggregate5.tags.add("AggregateTagA", "AggregateTagB")
118
- self.aggregate6.tags.add("AggregateTagB")
117
+ self.aggregate5.tags.add("AggregateTagA", "AggregateTagB") # pylint: disable=no-member
118
+ self.aggregate6.tags.add("AggregateTagB") # pylint: disable=no-member
119
119
 
120
120
  # notes
121
121
  self.note.objects.create(
@@ -141,7 +141,7 @@ class AggregateToPrefixMigrationTestCase(NautobotDataMigrationTest):
141
141
  self.note.objects.create(
142
142
  note="Aggregate5 test note",
143
143
  assigned_object_type=self.aggregate_ct,
144
- assigned_object_id=self.aggregate5.id,
144
+ assigned_object_id=self.aggregate5.id, # pylint: disable=no-member
145
145
  )
146
146
 
147
147
  # object permissions
@@ -157,8 +157,8 @@ class AggregateToPrefixMigrationTestCase(NautobotDataMigrationTest):
157
157
  # object changes
158
158
  self._create_objectchange(self.prefix1, "Pre-migration object change for prefix1")
159
159
  self._create_objectchange(self.prefix4, "Pre-migration object change for prefix4")
160
- self._create_objectchange(self.prefix5, "Pre-migration object change for prefix5")
161
- self._create_objectchange(self.aggregate5, "Pre-migration object change for aggregate5")
160
+ self._create_objectchange(self.prefix5, "Pre-migration object change for prefix5") # pylint: disable=no-member
161
+ self._create_objectchange(self.aggregate5, "Pre-migration object change for aggregate5") # pylint: disable=no-member
162
162
 
163
163
  # custom fields
164
164
  prefix_cf1 = self.custom_field.objects.create(name="prefixcf1")
@@ -179,24 +179,24 @@ class AggregateToPrefixMigrationTestCase(NautobotDataMigrationTest):
179
179
 
180
180
  self.aggregate3._custom_field_data["aggregatecf1"] = "testdata aggregatecf1 aggregate3"
181
181
 
182
- self.prefix5._custom_field_data["prefixcf1"] = "testdata prefixcf1 prefix5"
183
- self.prefix5._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 prefix5"
182
+ self.prefix5._custom_field_data["prefixcf1"] = "testdata prefixcf1 prefix5" # pylint: disable=no-member
183
+ self.prefix5._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 prefix5" # pylint: disable=no-member
184
184
 
185
- self.aggregate5._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 aggregate5"
186
- self.aggregate5._custom_field_data["aggregatecf1"] = "testdata aggregatecf1 aggregate5"
185
+ self.aggregate5._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 aggregate5" # pylint: disable=no-member
186
+ self.aggregate5._custom_field_data["aggregatecf1"] = "testdata aggregatecf1 aggregate5" # pylint: disable=no-member
187
187
 
188
- self.aggregate6._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 aggregate6"
188
+ self.aggregate6._custom_field_data["prefixaggregatecf1"] = "testdata prefixaggregatecf1 aggregate6" # pylint: disable=no-member
189
189
 
190
190
  self.prefix1.save()
191
191
  self.prefix2.save()
192
192
  self.prefix3.save()
193
193
  self.prefix4.save()
194
- self.prefix5.save()
194
+ self.prefix5.save() # pylint: disable=no-member
195
195
  self.aggregate1.save()
196
196
  self.aggregate2.save()
197
197
  self.aggregate3.save()
198
- self.aggregate5.save()
199
- self.aggregate6.save()
198
+ self.aggregate5.save() # pylint: disable=no-member
199
+ self.aggregate6.save() # pylint: disable=no-member
200
200
 
201
201
  @skipIf(
202
202
  connection.vendor != "postgresql",
@@ -390,7 +390,7 @@ class AggregateToPrefixMigrationTestCase(NautobotDataMigrationTest):
390
390
  2,
391
391
  )
392
392
 
393
- for prefix in (self.prefix2, self.prefix3, self.prefix5):
393
+ for prefix in (self.prefix2, self.prefix3, self.prefix5): # pylint: disable=no-member
394
394
  self.assertEqual(
395
395
  self.object_change.objects.filter(changed_object_id=prefix.id).count(),
396
396
  1,
@@ -478,6 +478,20 @@ class IPAddressQuerySet(TestCase):
478
478
  [instance for ip, instance in self.ips.items() if re.match(r"2001(.*)1", ip)],
479
479
  )
480
480
 
481
+ def test_get_or_create(self):
482
+ # https://github.com/nautobot/nautobot/issues/6676
483
+ ip_obj, created = IPAddress.objects.update_or_create(
484
+ defaults={
485
+ "status": self.ipaddr_status,
486
+ },
487
+ host="10.0.0.1",
488
+ mask_length="24",
489
+ namespace=self.namespace,
490
+ )
491
+ self.assertFalse(created)
492
+ self.assertEqual(str(ip_obj.address), "10.0.0.1/24")
493
+ self.assertEqual(ip_obj.parent.namespace, self.namespace)
494
+
481
495
 
482
496
  class PrefixQuerysetTestCase(TestCase):
483
497
  queryset = Prefix.objects.all()
@@ -10903,7 +10903,7 @@ as that is now handled by the NautobotCSVParser class and the REST API serialize
10903
10903
 
10904
10904
 
10905
10905
  <h3 id="nautobot.apps.forms.CSVFileField.to_python" class="doc doc-heading">
10906
- <code class="highlight language-python"><span class="n">to_python</span><span class="p">(</span><span class="n">file</span><span class="p">)</span></code>
10906
+ <code class="highlight language-python"><span class="n">to_python</span><span class="p">(</span><span class="n">data</span><span class="p">)</span></code>
10907
10907
 
10908
10908
  <a href="#nautobot.apps.forms.CSVFileField.to_python" class="headerlink" title="Permanent link">&para;</a></h3>
10909
10909
 
@@ -12773,7 +12773,7 @@ passed to the queryset's filter(field_name__in=[]) method but will fail to match
12773
12773
  <tr class="doc-section-item">
12774
12774
  <td><code>queryset</code></td>
12775
12775
  <td>
12776
- <code><autoref identifier="QuerySet" optional>QuerySet</autoref></code>
12776
+ <code><autoref identifier="django.db.models.QuerySet" optional hover>QuerySet</autoref></code>
12777
12777
  </td>
12778
12778
  <td>
12779
12779
  <div class="doc-md-description">
@@ -12081,7 +12081,7 @@ template_name: The name of the template</p>
12081
12081
  <tr class="doc-section-item">
12082
12082
  <td><code>instance</code></td>
12083
12083
  <td>
12084
- <code><autoref identifier="Model" optional>Model</autoref></code>
12084
+ <code><autoref identifier="django.db.models.Model" optional hover>Model</autoref></code>
12085
12085
  </td>
12086
12086
  <td>
12087
12087
  <div class="doc-md-description">
@@ -12776,7 +12776,7 @@ template_name: Name of the template to use</p>
12776
12776
  <tr class="doc-section-item">
12777
12777
  <td><code>instance</code></td>
12778
12778
  <td>
12779
- <code><autoref identifier="Model" optional>Model</autoref></code>
12779
+ <code><autoref identifier="django.db.models.Model" optional hover>Model</autoref></code>
12780
12780
  </td>
12781
12781
  <td>
12782
12782
  <div class="doc-md-description">