nautobot 2.2.1__py3-none-any.whl → 2.2.3__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.
Files changed (99) hide show
  1. nautobot/apps/jobs.py +2 -0
  2. nautobot/core/api/utils.py +12 -9
  3. nautobot/core/apps/__init__.py +2 -2
  4. nautobot/core/celery/__init__.py +79 -68
  5. nautobot/core/celery/backends.py +9 -1
  6. nautobot/core/celery/control.py +4 -7
  7. nautobot/core/celery/schedulers.py +4 -2
  8. nautobot/core/celery/task.py +78 -5
  9. nautobot/core/graphql/schema.py +2 -1
  10. nautobot/core/jobs/__init__.py +2 -1
  11. nautobot/core/templates/generic/object_list.html +3 -3
  12. nautobot/core/templatetags/helpers.py +66 -9
  13. nautobot/core/testing/__init__.py +6 -1
  14. nautobot/core/testing/api.py +12 -13
  15. nautobot/core/testing/mixins.py +2 -2
  16. nautobot/core/testing/views.py +50 -51
  17. nautobot/core/tests/test_api.py +23 -2
  18. nautobot/core/tests/test_templatetags_helpers.py +32 -0
  19. nautobot/core/tests/test_views.py +21 -1
  20. nautobot/core/tests/test_views_utils.py +22 -1
  21. nautobot/core/utils/module_loading.py +89 -0
  22. nautobot/core/views/generic.py +4 -4
  23. nautobot/core/views/mixins.py +4 -3
  24. nautobot/core/views/utils.py +3 -2
  25. nautobot/core/wsgi.py +9 -2
  26. nautobot/dcim/choices.py +14 -0
  27. nautobot/dcim/forms.py +59 -4
  28. nautobot/dcim/models/device_components.py +9 -5
  29. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +2 -2
  30. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +1 -1
  31. nautobot/dcim/templates/dcim/location.html +32 -13
  32. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +102 -0
  33. nautobot/dcim/tests/test_forms.py +49 -2
  34. nautobot/dcim/tests/test_views.py +137 -0
  35. nautobot/dcim/urls.py +5 -0
  36. nautobot/dcim/views.py +149 -1
  37. nautobot/extras/api/views.py +21 -10
  38. nautobot/extras/constants.py +3 -3
  39. nautobot/extras/context_managers.py +56 -0
  40. nautobot/extras/datasources/git.py +47 -58
  41. nautobot/extras/forms/forms.py +3 -1
  42. nautobot/extras/jobs.py +79 -146
  43. nautobot/extras/models/datasources.py +0 -2
  44. nautobot/extras/models/jobs.py +36 -18
  45. nautobot/extras/plugins/__init__.py +1 -20
  46. nautobot/extras/signals.py +88 -57
  47. nautobot/extras/test_jobs/__init__.py +8 -0
  48. nautobot/extras/test_jobs/dry_run.py +3 -2
  49. nautobot/extras/test_jobs/fail.py +43 -0
  50. nautobot/extras/test_jobs/ipaddress_vars.py +40 -1
  51. nautobot/extras/test_jobs/jobs_module/__init__.py +5 -0
  52. nautobot/extras/test_jobs/jobs_module/jobs_submodule/__init__.py +1 -0
  53. nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +6 -0
  54. nautobot/extras/test_jobs/pass.py +40 -0
  55. nautobot/extras/test_jobs/relative_import.py +11 -0
  56. nautobot/extras/tests/test_api.py +3 -0
  57. nautobot/extras/tests/test_context_managers.py +98 -1
  58. nautobot/extras/tests/test_datasources.py +125 -118
  59. nautobot/extras/tests/test_job_variables.py +57 -15
  60. nautobot/extras/tests/test_jobs.py +135 -1
  61. nautobot/extras/tests/test_models.py +26 -19
  62. nautobot/extras/tests/test_plugins.py +1 -3
  63. nautobot/extras/tests/test_views.py +2 -4
  64. nautobot/extras/utils.py +37 -0
  65. nautobot/extras/views.py +47 -95
  66. nautobot/ipam/api/views.py +8 -1
  67. nautobot/ipam/graphql/types.py +11 -0
  68. nautobot/ipam/mixins.py +32 -0
  69. nautobot/ipam/models.py +2 -1
  70. nautobot/ipam/querysets.py +6 -1
  71. nautobot/ipam/tables.py +1 -1
  72. nautobot/ipam/tests/test_models.py +82 -0
  73. nautobot/project-static/docs/assets/extra.css +4 -0
  74. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1 -1
  75. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +180 -211
  76. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +1 -1
  77. nautobot/project-static/docs/development/core/application-registry.html +126 -84
  78. nautobot/project-static/docs/development/core/model-checklist.html +49 -1
  79. nautobot/project-static/docs/development/core/model-features.html +1 -1
  80. nautobot/project-static/docs/development/jobs/index.html +334 -58
  81. nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
  82. nautobot/project-static/docs/objects.inv +0 -0
  83. nautobot/project-static/docs/release-notes/version-1.6.html +504 -201
  84. nautobot/project-static/docs/release-notes/version-2.2.html +392 -43
  85. nautobot/project-static/docs/search/search_index.json +1 -1
  86. nautobot/project-static/docs/sitemap.xml +254 -254
  87. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  88. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +7 -4
  89. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +111 -0
  90. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +15 -28
  91. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +4 -4
  92. nautobot/project-static/js/forms.js +18 -11
  93. {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/METADATA +3 -3
  94. {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/RECORD +98 -92
  95. nautobot/extras/test_jobs/job_variables.py +0 -93
  96. {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/LICENSE.txt +0 -0
  97. {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/NOTICE +0 -0
  98. {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/WHEEL +0 -0
  99. {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/entry_points.txt +0 -0
nautobot/ipam/models.py CHANGED
@@ -22,7 +22,7 @@ from nautobot.ipam import choices, constants
22
22
  from nautobot.virtualization.models import VMInterface
23
23
 
24
24
  from .fields import VarbinaryIPField
25
- from .querysets import IPAddressQuerySet, PrefixQuerySet, RIRQuerySet
25
+ from .querysets import IPAddressQuerySet, PrefixQuerySet, RIRQuerySet, VLANQuerySet
26
26
  from .validators import DNSValidator
27
27
 
28
28
  __all__ = (
@@ -1380,6 +1380,7 @@ class VLAN(PrimaryModel):
1380
1380
  ]
1381
1381
 
1382
1382
  natural_key_field_names = ["pk"]
1383
+ objects = BaseManager.from_queryset(VLANQuerySet)()
1383
1384
 
1384
1385
  class Meta:
1385
1386
  ordering = (
@@ -6,6 +6,7 @@ import netaddr
6
6
 
7
7
  from nautobot.core.models.querysets import RestrictedQuerySet
8
8
  from nautobot.core.utils.data import merge_dicts_without_collision
9
+ from nautobot.ipam.mixins import LocationToLocationsQuerySetMixin
9
10
 
10
11
 
11
12
  class RIRQuerySet(RestrictedQuerySet):
@@ -194,7 +195,7 @@ class BaseNetworkQuerySet(RestrictedQuerySet):
194
195
  return ip, last_ip
195
196
 
196
197
 
197
- class PrefixQuerySet(BaseNetworkQuerySet):
198
+ class PrefixQuerySet(LocationToLocationsQuerySetMixin, BaseNetworkQuerySet):
198
199
  """Queryset for `Prefix` objects."""
199
200
 
200
201
  def net_equals(self, *prefixes):
@@ -474,3 +475,7 @@ class IPAddressQuerySet(BaseNetworkQuerySet):
474
475
  q |= Q(pk__in=pk_values)
475
476
 
476
477
  return super().filter(q)
478
+
479
+
480
+ class VLANQuerySet(LocationToLocationsQuerySetMixin, RestrictedQuerySet):
481
+ """Queryset for `VLAN` objects."""
nautobot/ipam/tables.py CHANGED
@@ -771,7 +771,7 @@ class InterfaceVLANTable(StatusTableMixin, BaseTable):
771
771
  tenant = TenantColumn()
772
772
  role = tables.TemplateColumn(template_code=VLAN_ROLE_LINK)
773
773
  location_count = LinkedCountColumn(
774
- viewname="dcim:location",
774
+ viewname="dcim:location_list",
775
775
  url_params={"vlans": "pk"},
776
776
  verbose_name="Locations",
777
777
  )
@@ -295,6 +295,50 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
295
295
  self.child1 = Prefix.objects.create(prefix="101.102.0.0/26", status=self.status, namespace=self.namespace)
296
296
  self.child2 = Prefix.objects.create(prefix="101.102.0.64/26", status=self.status, namespace=self.namespace)
297
297
 
298
+ def test_location_queries(self):
299
+ locations = Location.objects.all()[:4]
300
+ for location in locations:
301
+ location.location_type.content_types.add(ContentType.objects.get_for_model(Prefix))
302
+ for i in range(10):
303
+ pfx = Prefix.objects.create(prefix=f"1.1.1.{4*i}/30", status=self.status, namespace=self.namespace)
304
+ if i > 4:
305
+ pfx.locations.set(locations)
306
+
307
+ with self.subTest("Assert filtering and excluding `location`"):
308
+ self.assertQuerysetEqualAndNotEmpty(
309
+ Prefix.objects.filter(location=locations[0]),
310
+ Prefix.objects.filter(locations__in=[locations[0]]),
311
+ )
312
+ self.assertQuerysetEqualAndNotEmpty(
313
+ Prefix.objects.exclude(location=locations[0]),
314
+ Prefix.objects.exclude(locations__in=[locations[0]]),
315
+ )
316
+ self.assertQuerysetEqualAndNotEmpty(
317
+ Prefix.objects.filter(location__in=[locations[0]]),
318
+ Prefix.objects.filter(locations__in=[locations[0]]),
319
+ )
320
+ self.assertQuerysetEqualAndNotEmpty(
321
+ Prefix.objects.exclude(location__in=[locations[0]]),
322
+ Prefix.objects.exclude(locations__in=[locations[0]]),
323
+ )
324
+
325
+ # We use `assertQuerysetEqualAndNotEmpty` for test validation. Including a nullable field could lead
326
+ # to flaky tests where querysets might return None, causing tests to fail. Therefore, we select
327
+ # fields that consistently contain values to ensure reliable filtering.
328
+ query_params = ["name", "location_type", "status"]
329
+
330
+ for field_name in query_params:
331
+ with self.subTest(f"Assert location__{field_name} query."):
332
+ value = getattr(locations[0], field_name)
333
+ self.assertQuerysetEqualAndNotEmpty(
334
+ Prefix.objects.filter(**{f"location__{field_name}": value}),
335
+ Prefix.objects.filter(**{f"locations__{field_name}": value}),
336
+ )
337
+ self.assertQuerysetEqualAndNotEmpty(
338
+ Prefix.objects.exclude(**{f"location__{field_name}": value}),
339
+ Prefix.objects.exclude(**{f"locations__{field_name}": value}),
340
+ )
341
+
298
342
  def test_prefix_validation(self):
299
343
  location_type = LocationType.objects.get(name="Room")
300
344
  location = Location.objects.filter(location_type=location_type).first()
@@ -1201,6 +1245,44 @@ class TestVLAN(ModelTestCases.BaseModelTestCase):
1201
1245
  location.vlans.add(vlan)
1202
1246
  self.assertIn(f"{location} is a Floor and may not have VLANs associated to it.", str(cm.exception))
1203
1247
 
1248
+ def test_location_queries(self):
1249
+ location = VLAN.objects.filter(locations__isnull=False).first().locations.first()
1250
+
1251
+ with self.subTest("Assert filtering and excluding `location`"):
1252
+ self.assertQuerysetEqualAndNotEmpty(
1253
+ VLAN.objects.filter(location=location),
1254
+ VLAN.objects.filter(locations__in=[location]),
1255
+ )
1256
+ self.assertQuerysetEqualAndNotEmpty(
1257
+ VLAN.objects.exclude(location=location),
1258
+ VLAN.objects.exclude(locations__in=[location]),
1259
+ )
1260
+ self.assertQuerysetEqualAndNotEmpty(
1261
+ VLAN.objects.filter(location__in=[location]),
1262
+ VLAN.objects.filter(locations__in=[location]),
1263
+ )
1264
+ self.assertQuerysetEqualAndNotEmpty(
1265
+ VLAN.objects.exclude(location__in=[location]),
1266
+ VLAN.objects.exclude(locations__in=[location]),
1267
+ )
1268
+
1269
+ # We use `assertQuerysetEqualAndNotEmpty` for test validation. Including a nullable field could lead
1270
+ # to flaky tests where querysets might return None, causing tests to fail. Therefore, we select
1271
+ # fields that consistently contain values to ensure reliable filtering.
1272
+ query_params = ["name", "location_type", "status"]
1273
+
1274
+ for field_name in query_params:
1275
+ with self.subTest(f"Assert location__{field_name} query."):
1276
+ value = getattr(location, field_name)
1277
+ self.assertQuerysetEqualAndNotEmpty(
1278
+ VLAN.objects.filter(**{f"location__{field_name}": value}),
1279
+ VLAN.objects.filter(**{f"locations__{field_name}": value}),
1280
+ )
1281
+ self.assertQuerysetEqualAndNotEmpty(
1282
+ VLAN.objects.exclude(**{f"location__{field_name}": value}),
1283
+ VLAN.objects.exclude(**{f"locations__{field_name}": value}),
1284
+ )
1285
+
1204
1286
 
1205
1287
  class TestVRF(ModelTestCases.BaseModelTestCase):
1206
1288
  model = VRF
@@ -137,3 +137,7 @@ img.copyright-logo {
137
137
  -webkit-mask-image: var(--md-admonition-icon--version-removed);
138
138
  mask-image: var(--md-admonition-icon--version-removed);
139
139
  }
140
+
141
+ .md-typeset .tabbed-set {
142
+ border: 0.5px solid gray;
143
+ }
@@ -11918,7 +11918,7 @@ data any serializer fields that do not correspond to a specific model field</p>
11918
11918
 
11919
11919
 
11920
11920
  <h2 id="nautobot.apps.api.get_view_name" class="doc doc-heading">
11921
- <code class="highlight language-python"><span class="n">nautobot</span><span class="o">.</span><span class="n">apps</span><span class="o">.</span><span class="n">api</span><span class="o">.</span><span class="n">get_view_name</span><span class="p">(</span><span class="n">view</span><span class="p">,</span> <span class="n">suffix</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span></code>
11921
+ <code class="highlight language-python"><span class="n">nautobot</span><span class="o">.</span><span class="n">apps</span><span class="o">.</span><span class="n">api</span><span class="o">.</span><span class="n">get_view_name</span><span class="p">(</span><span class="n">view</span><span class="p">)</span></code>
11922
11922
 
11923
11923
  <a href="#nautobot.apps.api.get_view_name" class="headerlink" title="Permanent link">&para;</a></h2>
11924
11924