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

@@ -31,11 +31,14 @@ class BulkEditObjects(Job):
31
31
  model=ContentType,
32
32
  description="Type of objects to update",
33
33
  )
34
+ # The names of the job inputs must match the parameters of `key_params` and get_bulk_queryset_from_view
35
+ # This may be confusing for the saved_view_id since the job input is an ObjectVar but the key_param is a PK
36
+ # But it is the lesser of two evils.
34
37
  form_data = JSONVar(description="BulkEditForm data")
35
38
  pk_list = JSONVar(description="List of objects pks to edit", required=False)
36
39
  edit_all = BooleanVar(description="Bulk Edit all object / all filtered objects", required=False)
37
40
  filter_query_params = JSONVar(label="Filter Query Params", required=False)
38
- saved_view = ObjectVar(model=SavedView, required=False)
41
+ saved_view_id = ObjectVar(model=SavedView, required=False)
39
42
 
40
43
  class Meta:
41
44
  name = "Bulk Edit Objects"
@@ -127,9 +130,9 @@ class BulkEditObjects(Job):
127
130
  raise RunJobTaskFailed("Bulk Edit not fully successful, see logs")
128
131
 
129
132
  def run( # pylint: disable=arguments-differ
130
- self, *, content_type, form_data, pk_list=None, edit_all=False, filter_query_params=None, saved_view=None
133
+ self, *, content_type, form_data, pk_list=None, edit_all=False, filter_query_params=None, saved_view_id=None
131
134
  ):
132
- saved_view_id = saved_view.pk if saved_view is not None else None
135
+ saved_view_id = saved_view_id.pk if saved_view_id is not None else None
133
136
  if not filter_query_params:
134
137
  filter_query_params = {}
135
138
 
@@ -186,10 +189,13 @@ class BulkDeleteObjects(Job):
186
189
  model=ContentType,
187
190
  description="Type of objects to delete",
188
191
  )
192
+ # The names of the job inputs must match the parameters of `key_params` and get_bulk_queryset_from_view
193
+ # This may be confusing for the saved_view_id since the job input is an ObjectVar but the key_param is a PK
194
+ # But it is the lesser of two evils.
189
195
  pk_list = JSONVar(description="List of objects pks to delete", required=False)
190
196
  delete_all = BooleanVar(description="Delete all (filtered) objects instead of a list of PKs", required=False)
191
197
  filter_query_params = JSONVar(label="Filter Query Params", required=False)
192
- saved_view = ObjectVar(model=SavedView, required=False)
198
+ saved_view_id = ObjectVar(model=SavedView, required=False)
193
199
 
194
200
  class Meta:
195
201
  name = "Bulk Delete Objects"
@@ -200,9 +206,9 @@ class BulkDeleteObjects(Job):
200
206
  hidden = True
201
207
 
202
208
  def run( # pylint: disable=arguments-differ
203
- self, *, content_type, pk_list=None, delete_all=False, filter_query_params=None, saved_view=None
209
+ self, *, content_type, pk_list=None, delete_all=False, filter_query_params=None, saved_view_id=None
204
210
  ):
205
- saved_view_id = saved_view.pk if saved_view is not None else None
211
+ saved_view_id = saved_view_id.pk if saved_view_id is not None else None
206
212
  if not filter_query_params:
207
213
  filter_query_params = {}
208
214
  if not self.user.has_perm(f"{content_type.app_label}.delete_{content_type.model}"):
@@ -1,7 +1,7 @@
1
1
  from datetime import timedelta
2
2
 
3
3
  from django.core.exceptions import PermissionDenied
4
- from django.db.models import CASCADE
4
+ from django.db.models import CASCADE, PROTECT
5
5
  from django.db.models.signals import pre_delete
6
6
  from django.utils import timezone
7
7
 
@@ -67,6 +67,18 @@ class LogsCleanup(Job):
67
67
  cascade_queryset = related_model.objects.filter(**{f"{related_field_name}__id__in": queryset})
68
68
  if cascade_queryset.exists():
69
69
  self.recursive_delete_with_cascade(cascade_queryset, deletion_summary)
70
+ elif related_object.on_delete is PROTECT:
71
+ self.logger.warning(
72
+ "Skipping %s records with a protected relationship to %s."
73
+ " You must delete the related object(s) first.",
74
+ queryset.model._meta.label,
75
+ related_object.related_model._meta.label,
76
+ )
77
+ items_to_exclude = related_object.related_model.objects.values_list(
78
+ related_object.field.name, flat=True
79
+ )
80
+ queryset = queryset.exclude(id__in=items_to_exclude)
81
+ deletion_summary.update({related_object.related_model._meta.label: 0})
70
82
 
71
83
  genericrelation_related_fields = [
72
84
  field for field in queryset.model._meta.private_fields if hasattr(field, "bulk_related_objects")
@@ -123,7 +123,17 @@ def setup_structlog_logging(
123
123
  return
124
124
 
125
125
  django_apps.append("django_structlog")
126
- django_middleware.append("django_structlog.middlewares.RequestMiddleware")
126
+
127
+ # Insert the middleware ahead of django_prometheus.middleware.PrometheusAfterMiddleware, which consumes the request.
128
+ # If that middleware is not present, append it at the end.
129
+ django_structlog_middleware = "django_structlog.middlewares.RequestMiddleware"
130
+ try:
131
+ index_of_prometheus_after_middleware = django_middleware.index(
132
+ "django_prometheus.middleware.PrometheusAfterMiddleware"
133
+ )
134
+ django_middleware.insert(index_of_prometheus_after_middleware, django_structlog_middleware)
135
+ except ValueError:
136
+ django_middleware.append(django_structlog_middleware)
127
137
 
128
138
  processors = (
129
139
  # Add the log level to the event dict under the level key.
@@ -1060,6 +1060,70 @@ class BulkEditTestCase(TransactionTestCase):
1060
1060
  )
1061
1061
  self.assertEqual(IPAddress.objects.all().count(), IPAddress.objects.filter(status=active_status).count())
1062
1062
 
1063
+ def test_bulk_edit_objects_with_saved_view(self):
1064
+ """
1065
+ Bulk edit Status objects using a SavedView filter.
1066
+ """
1067
+ self.add_permissions("extras.change_status", "extras.view_status")
1068
+ saved_view = SavedView.objects.create(
1069
+ name="Save View for Statuses",
1070
+ owner=self.user,
1071
+ view="extras:status_list",
1072
+ config={"filter_params": {"name__isw": ["A"]}},
1073
+ )
1074
+
1075
+ # Confirm the SavedView filter matches some but not all Statuses
1076
+ self.assertTrue(
1077
+ 0 < saved_view.get_filtered_queryset(self.user).count() < Status.objects.exclude(color="aa1409").count()
1078
+ )
1079
+ delta_count = (
1080
+ Status.objects.exclude(color="aa1409").count() - saved_view.get_filtered_queryset(self.user).count()
1081
+ )
1082
+
1083
+ create_job_result_and_run_job(
1084
+ "nautobot.core.jobs.bulk_actions",
1085
+ "BulkEditObjects",
1086
+ username=self.user.username,
1087
+ content_type=self.status_ct.id,
1088
+ edit_all=True,
1089
+ filter_query_params={},
1090
+ pk_list=[],
1091
+ saved_view_id=saved_view.id,
1092
+ form_data={"color": "aa1409", "_all": "True"},
1093
+ )
1094
+
1095
+ self.assertEqual(delta_count, Status.objects.exclude(color="aa1409").count())
1096
+
1097
+ def test_bulk_edit_objects_with_saved_view_with_all_filters_removed(self):
1098
+ """
1099
+ Bulk edit Status objects using a SavedView filter but overwriting the saved field.
1100
+ """
1101
+ self.add_permissions("extras.change_status", "extras.view_status")
1102
+ saved_view = SavedView.objects.create(
1103
+ name="Save View for Statuses",
1104
+ owner=self.user,
1105
+ view="extras:status_list",
1106
+ config={"filter_params": {"name__isw": ["A"]}},
1107
+ )
1108
+
1109
+ self.assertTrue(
1110
+ 0 < saved_view.get_filtered_queryset(self.user).count() < Status.objects.exclude(color="aa1409").count()
1111
+ )
1112
+
1113
+ create_job_result_and_run_job(
1114
+ "nautobot.core.jobs.bulk_actions",
1115
+ "BulkEditObjects",
1116
+ username=self.user.username,
1117
+ content_type=self.status_ct.id,
1118
+ edit_all=True,
1119
+ filter_query_params={"all_filters_removed": [True]},
1120
+ pk_list=[],
1121
+ saved_view_id=saved_view.id,
1122
+ form_data={"color": "aa1409", "_all": "True"},
1123
+ )
1124
+
1125
+ self.assertEqual(0, Status.objects.exclude(color="aa1409").count())
1126
+
1063
1127
 
1064
1128
  class BulkDeleteTestCase(TransactionTestCase):
1065
1129
  """
@@ -1099,6 +1163,19 @@ class BulkDeleteTestCase(TransactionTestCase):
1099
1163
  circuit_type=circuit_type,
1100
1164
  status=statuses[0],
1101
1165
  )
1166
+ Circuit.objects.create(
1167
+ cid="Not Circuit",
1168
+ provider=provider,
1169
+ circuit_type=circuit_type,
1170
+ status=statuses[0],
1171
+ )
1172
+
1173
+ self.saved_view = SavedView.objects.create(
1174
+ name="Save View for Circuits",
1175
+ owner=self.user,
1176
+ view="circuits:circuit_list",
1177
+ config={"filter_params": {"cid__isw": "Circuit "}},
1178
+ )
1102
1179
 
1103
1180
  def _common_no_error_test_assertion(self, model, job_result, **filter_params):
1104
1181
  self.assertJobResultStatus(job_result)
@@ -1250,6 +1327,47 @@ class BulkDeleteTestCase(TransactionTestCase):
1250
1327
  )
1251
1328
  self._common_no_error_test_assertion(Role, job_result, name__istartswith="Example Status")
1252
1329
 
1330
+ def test_bulk_delete_objects_with_saved_view(self):
1331
+ """
1332
+ Delete objects using a SavedView filter.
1333
+ """
1334
+ self.add_permissions("circuits.delete_circuit", "circuits.view_circuit")
1335
+
1336
+ # we assert that the saved view filter actually filters some circuits and there are others not filtered out
1337
+ self.assertTrue(0 < self.saved_view.get_filtered_queryset(self.user).count() < Circuit.objects.all().count())
1338
+ create_job_result_and_run_job(
1339
+ "nautobot.core.jobs.bulk_actions",
1340
+ "BulkDeleteObjects",
1341
+ username=self.user.username,
1342
+ content_type=ContentType.objects.get_for_model(Circuit).id,
1343
+ delete_all=True,
1344
+ filter_query_params={},
1345
+ pk_list=[],
1346
+ saved_view_id=self.saved_view.id,
1347
+ )
1348
+ self.assertTrue(Circuit.objects.exists())
1349
+ self.assertFalse(self.saved_view.get_filtered_queryset(self.user).exists())
1350
+
1351
+ def test_bulk_delete_objects_with_saved_view_with_all_filters_removed(self):
1352
+ """
1353
+ Delete Objects using a SavedView filter, but ignore the filter if all_filters_removed is set.
1354
+ """
1355
+ self.add_permissions("circuits.delete_circuit", "circuits.view_circuit")
1356
+
1357
+ # we assert that the saved view filter actually filters some circuits and there are others not filtered out
1358
+ self.assertTrue(0 < self.saved_view.get_filtered_queryset(self.user).count() < Circuit.objects.all().count())
1359
+ create_job_result_and_run_job(
1360
+ "nautobot.core.jobs.bulk_actions",
1361
+ "BulkDeleteObjects",
1362
+ username=self.user.username,
1363
+ content_type=ContentType.objects.get_for_model(Circuit).id,
1364
+ delete_all=True,
1365
+ filter_query_params={"all_filters_removed": [True]},
1366
+ pk_list=[],
1367
+ saved_view_id=self.saved_view.id,
1368
+ )
1369
+ self.assertFalse(Circuit.objects.all().exists())
1370
+
1253
1371
 
1254
1372
  class RefreshDynamicGroupCacheJobButtonReceiverTestCase(TransactionTestCase):
1255
1373
  def setUp(self):
@@ -1049,7 +1049,8 @@ class ObjectEditViewMixin(NautobotViewSetMixin, mixins.CreateModelMixin, mixins.
1049
1049
  if hasattr(obj, "clone_fields"):
1050
1050
  url = f"{request.path}?{prepare_cloned_fields(obj)}"
1051
1051
  self.success_url = url
1052
- self.success_url = request.get_full_path()
1052
+ else:
1053
+ self.success_url = request.get_full_path()
1053
1054
  else:
1054
1055
  return_url = form.cleaned_data.get("return_url")
1055
1056
  if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
@@ -586,9 +586,9 @@ def get_bulk_queryset_from_view(
586
586
 
587
587
  queryset = view_class.queryset.restrict(user, action)
588
588
 
589
- # The filterset_class is determined from model on purpose, as filterset_class the view as a param, will not work
590
- # with a job. It is better to be consistent with each with sending the same params that will
591
- # always be available from to the confirmation page and to the job.
589
+ # The filterset_class is determined from model on purpose versus getting it from the view itself. This is
590
+ # because the filterset_class on the view as a param, will not work with a job. It is better to be consistent
591
+ # with each with sending the same params that will always be available from to the confirmation page and to the job.
592
592
  filterset_class = get_filterset_for_model(model)
593
593
 
594
594
  if not filterset_class:
nautobot/extras/jobs.py CHANGED
@@ -29,6 +29,7 @@ from django.db.models.query import QuerySet
29
29
  from django.forms import ValidationError
30
30
  from django.utils.functional import classproperty
31
31
  import netaddr
32
+ from prometheus_client import Counter
32
33
  import yaml
33
34
 
34
35
  from nautobot.core.celery import import_jobs, nautobot_task
@@ -88,6 +89,27 @@ __all__ = [
88
89
 
89
90
  logger = logging.getLogger(__name__)
90
91
 
92
+ started_jobs_counter = Counter(
93
+ name="nautobot_worker_started_jobs",
94
+ documentation="Job executions that started running",
95
+ labelnames=("job_class_name", "module_name"),
96
+ )
97
+ finished_jobs_counter = Counter(
98
+ name="nautobot_worker_finished_jobs",
99
+ documentation="Job executions that finished running",
100
+ labelnames=("job_class_name", "module_name", "status"),
101
+ )
102
+ exception_jobs_counter = Counter(
103
+ name="nautobot_worker_exception_jobs",
104
+ documentation="Job executions that raised an exception",
105
+ labelnames=("job_class_name", "module_name", "exception_type"),
106
+ )
107
+ singleton_conflict_counter = Counter(
108
+ name="nautobot_worker_singleton_conflict",
109
+ documentation="Job executions that ran into a singleton lock",
110
+ labelnames=("job_class_name", "module_name"),
111
+ )
112
+
91
113
 
92
114
  class RunJobTaskFailed(Exception):
93
115
  """Celery task failed for some reason."""
@@ -1198,6 +1220,9 @@ def _prepare_job(job_class_path, request, kwargs) -> tuple[Job, dict]:
1198
1220
  extra={"object": job.job_model, "grouping": "initialization"},
1199
1221
  )
1200
1222
  else:
1223
+ singleton_conflict_counter.labels(
1224
+ job_class_name=job.job_model.job_class_name, module_name=job.job_model.module_name
1225
+ ).inc()
1201
1226
  # TODO 3.0: maybe change to logger.failure() and return cleanly, as this is an "acceptable" failure?
1202
1227
  job.logger.error(
1203
1228
  "Job %s is a singleton and already running.",
@@ -1281,6 +1306,9 @@ def run_job(self, job_class_path, *args, **kwargs):
1281
1306
 
1282
1307
  result = None
1283
1308
  status = None
1309
+ started_jobs_counter.labels(
1310
+ job_class_name=job.job_model.job_class_name, module_name=job.job_model.module_name
1311
+ ).inc()
1284
1312
  try:
1285
1313
  before_start_result = job.before_start(self.request.id, args, kwargs)
1286
1314
  if not job._failed:
@@ -1297,6 +1325,9 @@ def run_job(self, job_class_path, *args, **kwargs):
1297
1325
  job.on_success(result, self.request.id, args, kwargs)
1298
1326
  else:
1299
1327
  job.on_failure(result, self.request.id, args, kwargs, None)
1328
+ finished_jobs_counter.labels(
1329
+ job_class_name=job.job_model.job_class_name, module_name=job.job_model.module_name, status=status
1330
+ ).inc()
1300
1331
 
1301
1332
  job.after_return(status, result, self.request.id, args, kwargs, None)
1302
1333
 
@@ -1313,11 +1344,21 @@ def run_job(self, job_class_path, *args, **kwargs):
1313
1344
  # We don't want to overwrite the manual state update that we did above, so:
1314
1345
  raise Ignore()
1315
1346
 
1316
- except Reject:
1347
+ except Reject as exc:
1348
+ exception_jobs_counter.labels(
1349
+ job_class_name=job.job_model.job_class_name,
1350
+ module_name=job.job_model.module_name,
1351
+ exception_type=type(exc).__name__,
1352
+ ).inc()
1317
1353
  status = status or JobResultStatusChoices.STATUS_REJECTED
1318
1354
  raise
1319
1355
 
1320
- except Ignore:
1356
+ except Ignore as exc:
1357
+ exception_jobs_counter.labels(
1358
+ job_class_name=job.job_model.job_class_name,
1359
+ module_name=job.job_model.module_name,
1360
+ exception_type=type(exc).__name__,
1361
+ ).inc()
1321
1362
  status = status or JobResultStatusChoices.STATUS_IGNORED
1322
1363
  raise
1323
1364
 
@@ -1330,6 +1371,11 @@ def run_job(self, job_class_path, *args, **kwargs):
1330
1371
  "exc_type": type(exc).__name__,
1331
1372
  "exc_message": sanitize(str(exc)),
1332
1373
  }
1374
+ exception_jobs_counter.labels(
1375
+ job_class_name=job.job_model.job_class_name,
1376
+ module_name=job.job_model.module_name,
1377
+ exception_type=type(exc).__name__,
1378
+ ).inc()
1333
1379
  raise
1334
1380
 
1335
1381
  finally:
@@ -26,6 +26,7 @@ from nautobot.core.models import BaseManager, BaseModel
26
26
  from nautobot.core.models.fields import ForeignKeyWithAutoRelatedName, LaxURLField
27
27
  from nautobot.core.models.generics import OrganizationalModel, PrimaryModel
28
28
  from nautobot.core.utils.data import deepmerge, render_jinja2
29
+ from nautobot.core.utils.lookup import get_filterset_for_model, get_model_for_view_name
29
30
  from nautobot.extras.choices import (
30
31
  ButtonClassChoices,
31
32
  WebhookHttpMethodChoices,
@@ -903,6 +904,24 @@ class SavedView(BaseModel, ChangeLoggedModel):
903
904
 
904
905
  super().save(*args, **kwargs)
905
906
 
907
+ @property
908
+ def model(self):
909
+ """
910
+ Return the model class associated with this SavedView, based on the 'view' field.
911
+ """
912
+ return get_model_for_view_name(self.view)
913
+
914
+ def get_filtered_queryset(self, user):
915
+ """
916
+ Return a queryset for the associated model, filtered by this SavedView's filter_params.
917
+ """
918
+ model = self.model
919
+ if model is None:
920
+ return None
921
+ filter_params = self.config.get("filter_params", {})
922
+ filterset_class = get_filterset_for_model(model)
923
+ return filterset_class(filter_params, model.objects.restrict(user)).qs
924
+
906
925
 
907
926
  @extras_features("graphql")
908
927
  class UserSavedViewAssociation(BaseModel):
@@ -284,7 +284,9 @@ class RelationshipModel(models.Model):
284
284
  Q(**side_query_params) | Q(**peer_side_query_params)
285
285
  ).distinct()
286
286
  if not relationship.has_many(peer_side):
287
- resp[side][relationship] = resp[side][relationship].first()
287
+ resp[RelationshipSideChoices.SIDE_PEER][relationship] = resp[
288
+ RelationshipSideChoices.SIDE_PEER
289
+ ][relationship].first()
288
290
  else:
289
291
  # Maybe an uninstalled App?
290
292
  # We can't provide a relevant queryset, but we can provide a descriptive string
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nautobot
3
- Version: 2.4.22
3
+ Version: 2.4.23
4
4
  Summary: Source of truth and network automation platform.
5
5
  License: Apache-2.0
6
6
  Keywords: Nautobot
@@ -186,8 +186,8 @@ nautobot/core/graphql/schema_init.py,sha256=p5z7usFxshmEII1akimwaXDRTqMtpAFcFn4F
186
186
  nautobot/core/graphql/types.py,sha256=_I-J0S5HFUmG4Hwt2sIbAoSllERZRQl-uoR6MwI7V6E,1547
187
187
  nautobot/core/graphql/utils.py,sha256=cfRHdMSXbBCFq4HcNUOvZ5ibezUa-k9EFh7UcExRq4Y,4740
188
188
  nautobot/core/jobs/__init__.py,sha256=9vVWZce5T4xt-wITfrOOY1iumYR30M-_gURgyv3Dk3U,17195
189
- nautobot/core/jobs/bulk_actions.py,sha256=0e4HgbgBjnLHWox1vzJ0YFLxA56FfJTg1_mmPMasnIs,11496
190
- nautobot/core/jobs/cleanup.py,sha256=dPdZVSNh19HvS6K0TCNC-B8-ZNy3jWy7q3fbaqodytI,6627
189
+ nautobot/core/jobs/bulk_actions.py,sha256=Nr0eGbFv3HuUd8_ym2g-w5VyUksKgLXnTY6AyC56RZo,12042
190
+ nautobot/core/jobs/cleanup.py,sha256=yc9yScRjIU3XjGl8sisWIZna7RDKK4YT7tUVweRkKwY,7319
191
191
  nautobot/core/jobs/groups.py,sha256=_vL5bdWXUoWtEGSlQYLbNGZ7JYwfgGAZViYRfePWrKE,2790
192
192
  nautobot/core/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
193
193
  nautobot/core/management/commands/__init__.py,sha256=rzxYmNIH9WLuO6OE-zOnOk6ojQGK-EjoRcT1vKWO60M,696
@@ -221,7 +221,7 @@ nautobot/core/models/validators.py,sha256=ckZuqwAjxFb1b_X5t0j3oPqmDv2TvDsYC-OJKO
221
221
  nautobot/core/releases.py,sha256=fqbWLBaYI8rlxfCGDhOaUCpbHAbFHGIsNX9DWgw0XeE,1214
222
222
  nautobot/core/settings.py,sha256=lJXrXMFR3SpQ3l-02W-dANb1EDw7Ar4A4Z2mpiUxVLo,51425
223
223
  nautobot/core/settings.yaml,sha256=tGaN8M9I7Zh2fbTs9ERrGjyU-kuLkyKd1x8G1MDHW_4,88469
224
- nautobot/core/settings_funcs.py,sha256=QQ_olczPhH0r6yT6qNZ6YAqTEK06ZkhemprvAHv9PR0,6579
224
+ nautobot/core/settings_funcs.py,sha256=5GjeyajETxtn-FEglDOCErn3VIjR1XzDtthdc6oCl7Q,7113
225
225
  nautobot/core/signals.py,sha256=2fLrxqk8OM3YfG8dkFkv_f0ddRfjjz7IDUY9B470s2c,3330
226
226
  nautobot/core/tables.py,sha256=JRZgwg_6W6Y--LJjGJncBMVs9UNMT0LwaUxyWDelEDM,33269
227
227
  nautobot/core/tasks.py,sha256=eitDpGSum6DHxeI7lCm_sUMHEaOC1SFx0TFcmC8txow,2102
@@ -420,7 +420,7 @@ nautobot/core/tests/test_filters.py,sha256=WWjxQPk_CTJGKUHbeQQdVJ91iyfWO1l6ZFynl
420
420
  nautobot/core/tests/test_forms.py,sha256=GL6kQyHyrrgMbdJlC0YkYu9ugqZWoT87Q-X4BsVbfz8,37718
421
421
  nautobot/core/tests/test_graphql.py,sha256=6epvCjMy2FdR7byLViShbeo6JHxUZTL3aH5Gf_NsY3Q,109738
422
422
  nautobot/core/tests/test_jinja_filters.py,sha256=y5MqljKR0SyfVP-yXdy6OrcID0_3aURfDoUg-zme5Jc,6088
423
- nautobot/core/tests/test_jobs.py,sha256=wH-b885VULWVK-GQJiocJ72l6cGvC-oMRNF3-y92Ebg,61127
423
+ nautobot/core/tests/test_jobs.py,sha256=pRXdf0XJJrr-l0IwQDYRtNG2rgVJih_2ryqDdVH0xvM,65945
424
424
  nautobot/core/tests/test_logging.py,sha256=rmuKmhWEac2HBZMn27GA9c9LEjFshzwTDmXnXukWXvk,3043
425
425
  nautobot/core/tests/test_managers.py,sha256=31PqBV_T83ZLoYxpKr-Zo0wD9MC366l-OBrjfLnaTOM,5653
426
426
  nautobot/core/tests/test_models.py,sha256=8YpWxVl77pSrDzx9MTaOsKOto0AFFUKVBnClXgcJNQc,9521
@@ -472,11 +472,11 @@ nautobot/core/utils/querysets.py,sha256=Fsftouekyf8POFNQfDJhLBVLbJr2dtpZsleEFFtp
472
472
  nautobot/core/utils/requests.py,sha256=IPI_zCJXAfucnRubnsUE1YRghVnKfK238qHx1mZ2gpY,10318
473
473
  nautobot/core/views/__init__.py,sha256=crfYo3RE9uD0H7EeIaKO7uDIHZRqULkHtlxPbqLiaos,24220
474
474
  nautobot/core/views/generic.py,sha256=gysktVxctpirbEgeiZ4sGciOroLRd8ze0sF9iVqVwQ4,67227
475
- nautobot/core/views/mixins.py,sha256=5QKoB3vGIQhF1uUiji2bvEfqOu5cWuhHFremvOXS2JY,65187
475
+ nautobot/core/views/mixins.py,sha256=DAqfeifXgJgYdSe6nzayZrCcdti3FxjspLq9BupiscU,65213
476
476
  nautobot/core/views/paginator.py,sha256=EXGMQBOHNbczuSIR-2lsL2O-dRAV5R2qpjqtuV90O9E,2694
477
477
  nautobot/core/views/renderers.py,sha256=CXBgQzXhy_0J1B4k0cmqu3vUTZh1qePWLeOMLjfDlks,18778
478
478
  nautobot/core/views/routers.py,sha256=xdfNWuMRKF5woyrH3ISMDf8Y_ajSWMf0LTUMW0fs9bQ,2706
479
- nautobot/core/views/utils.py,sha256=2gr8igQ5kxzuxT9b6RPE7T-pmSY2nY1ixKXczC-bcG0,29719
479
+ nautobot/core/views/utils.py,sha256=Ytrs5BUFeO7Sa75WQoQBJJB-LpeoF9Ww0YmTwH7WaCA,29778
480
480
  nautobot/core/views/viewsets.py,sha256=cqp9un4F9n4-TlZ7iVks-0w3IjSQxcex-bFYo490BGs,727
481
481
  nautobot/core/wsgi.py,sha256=cujlOp6n3G0IjNSR6FMEzkIBV2uQI8UK7u3lEE7j1Xs,1184
482
482
  nautobot/dcim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -809,7 +809,7 @@ nautobot/extras/graphql/types.py,sha256=agY80xhUNXeUTXVJoysO-FqvFEDsuPo9tb5FoX5t
809
809
  nautobot/extras/group_sync.py,sha256=ZPeniNgB0mmDV6NARtx7gxTxeKRCZDebgkKbyBQ5RMI,1611
810
810
  nautobot/extras/health_checks.py,sha256=A0R8ste1lpb5_dzamqvt6GcNDjcfQbWqreHbgCZZDrs,6810
811
811
  nautobot/extras/homepage.py,sha256=_Ie_hBqSINcLxti0wWm40KoZpOB8ImNGE9EeAHgutWE,2160
812
- nautobot/extras/jobs.py,sha256=KOMhBniabXRv5B0UhB2NIMb3_sGmYihxkaGl4YOgiJw,49996
812
+ nautobot/extras/jobs.py,sha256=1wyXov19K9qfvrEtjk8X-pGPO78ADEZskKX1UsqHM0o,51946
813
813
  nautobot/extras/jobs_ui.py,sha256=cz85ODLooOgfsOIEt4pabhY25I3wrwD6LZNlE6MeKsM,8995
814
814
  nautobot/extras/management/__init__.py,sha256=FcUvZsw5OhOflIEitrzkKRu9mBrL4fTlF5_823m5lkE,16343
815
815
  nautobot/extras/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -957,8 +957,8 @@ nautobot/extras/models/groups.py,sha256=bxzZ4rkIQC_jqJ5GBHabcJy8zptDCHXnlF_jnR1H
957
957
  nautobot/extras/models/jobs.py,sha256=s4lT0i0oDFTnjREeP7ZDYTsFZrRX-YpmX5gY-QWxT_E,59518
958
958
  nautobot/extras/models/metadata.py,sha256=eHozr4w2hdDFRKVUfZEuiu7KsShTav8PUpiThc9EUvQ,20634
959
959
  nautobot/extras/models/mixins.py,sha256=uOfakOMjmX3Wt4PU23NP9ZZumeKTQ21CqxL8k-BTBFc,5429
960
- nautobot/extras/models/models.py,sha256=vT6s1LPT93FpcyBvbgdvuruGLUwqm-A3x68BtNPs9C8,40997
961
- nautobot/extras/models/relationships.py,sha256=z0WWGSByPsKpOUi0kuL2JXNO4tUPV4rjoQOYBG8Qal0,50997
960
+ nautobot/extras/models/models.py,sha256=ErCFNwGmpZ15jDCSul32QGe1q0ECu3AdgeNQYLG2aVc,41723
961
+ nautobot/extras/models/relationships.py,sha256=PuurAcIqSE3JstbCrrct6t-7Ozh7qM6ofeKjDFoZklQ,51117
962
962
  nautobot/extras/models/roles.py,sha256=IoE2zlVJTUHNY8_iMtTaJgrmBBGwYHvDeAJnluXhNbw,1204
963
963
  nautobot/extras/models/secrets.py,sha256=bA1HQ2l0OIfEk8aTsM9wF4ROEq2dy865-lzi0NN0--8,5788
964
964
  nautobot/extras/models/statuses.py,sha256=zZLjPLDQsVUxrZmmeoq571q0lM5hMfMt2bTAhBjkv1U,4093
@@ -1681,9 +1681,9 @@ nautobot/wireless/tests/test_models.py,sha256=Fpqc8H7qxXhlM8M8EuBVxTu623L58AHW_e
1681
1681
  nautobot/wireless/tests/test_views.py,sha256=_387uMzc_F9xgxdRGu81PkVyDLmNFb1J-vXt3PdQGFA,18781
1682
1682
  nautobot/wireless/urls.py,sha256=yfYcx1WHLx99pBesoaF602_fUQLXHtodWOi7XHtuX4c,395
1683
1683
  nautobot/wireless/views.py,sha256=Mgj-1yUuPuP5_qV-WaQ8ABp4g9fKr9qJlL15qx5nG9I,5472
1684
- nautobot-2.4.22.dist-info/LICENSE.txt,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
1685
- nautobot-2.4.22.dist-info/METADATA,sha256=4CnQJaJHoZ0Ak-Jd8yYsCIidditdLhLQyYTM498r_-A,9895
1686
- nautobot-2.4.22.dist-info/NOTICE,sha256=RA2yQ-u70Ex-APSWYkMN6IdM7zp7cWK0SzmVrqBCcUA,284
1687
- nautobot-2.4.22.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
1688
- nautobot-2.4.22.dist-info/entry_points.txt,sha256=42BEJlv0XylqjTQ8gHtZeW-edvylzk8PmlPYe7kf8Fw,148
1689
- nautobot-2.4.22.dist-info/RECORD,,
1684
+ nautobot-2.4.23.dist-info/LICENSE.txt,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
1685
+ nautobot-2.4.23.dist-info/METADATA,sha256=XeM-EGcvPyPfypOZu0y3gwQ7ziKY5JiG9zY1MjoBSts,9895
1686
+ nautobot-2.4.23.dist-info/NOTICE,sha256=RA2yQ-u70Ex-APSWYkMN6IdM7zp7cWK0SzmVrqBCcUA,284
1687
+ nautobot-2.4.23.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
1688
+ nautobot-2.4.23.dist-info/entry_points.txt,sha256=42BEJlv0XylqjTQ8gHtZeW-edvylzk8PmlPYe7kf8Fw,148
1689
+ nautobot-2.4.23.dist-info/RECORD,,