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.
- nautobot/core/jobs/bulk_actions.py +12 -6
- nautobot/core/jobs/cleanup.py +13 -1
- nautobot/core/settings_funcs.py +11 -1
- nautobot/core/tests/test_jobs.py +118 -0
- nautobot/core/views/mixins.py +2 -1
- nautobot/core/views/utils.py +3 -3
- nautobot/extras/jobs.py +48 -2
- nautobot/extras/models/models.py +19 -0
- nautobot/extras/models/relationships.py +3 -1
- {nautobot-2.4.22.dist-info → nautobot-2.4.23.dist-info}/METADATA +1 -1
- {nautobot-2.4.22.dist-info → nautobot-2.4.23.dist-info}/RECORD +15 -15
- {nautobot-2.4.22.dist-info → nautobot-2.4.23.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.22.dist-info → nautobot-2.4.23.dist-info}/NOTICE +0 -0
- {nautobot-2.4.22.dist-info → nautobot-2.4.23.dist-info}/WHEEL +0 -0
- {nautobot-2.4.22.dist-info → nautobot-2.4.23.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
209
|
+
self, *, content_type, pk_list=None, delete_all=False, filter_query_params=None, saved_view_id=None
|
|
204
210
|
):
|
|
205
|
-
saved_view_id =
|
|
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}"):
|
nautobot/core/jobs/cleanup.py
CHANGED
|
@@ -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")
|
nautobot/core/settings_funcs.py
CHANGED
|
@@ -123,7 +123,17 @@ def setup_structlog_logging(
|
|
|
123
123
|
return
|
|
124
124
|
|
|
125
125
|
django_apps.append("django_structlog")
|
|
126
|
-
|
|
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.
|
nautobot/core/tests/test_jobs.py
CHANGED
|
@@ -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):
|
nautobot/core/views/mixins.py
CHANGED
|
@@ -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
|
-
|
|
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()):
|
nautobot/core/views/utils.py
CHANGED
|
@@ -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
|
|
590
|
-
# with a job. It is better to be consistent
|
|
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:
|
nautobot/extras/models/models.py
CHANGED
|
@@ -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[
|
|
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
|
|
@@ -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=
|
|
190
|
-
nautobot/core/jobs/cleanup.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
961
|
-
nautobot/extras/models/relationships.py,sha256=
|
|
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.
|
|
1685
|
-
nautobot-2.4.
|
|
1686
|
-
nautobot-2.4.
|
|
1687
|
-
nautobot-2.4.
|
|
1688
|
-
nautobot-2.4.
|
|
1689
|
-
nautobot-2.4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|