nautobot 2.4.21__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/apps/choices.py +4 -0
- nautobot/apps/utils.py +8 -0
- nautobot/circuits/views.py +6 -2
- nautobot/core/cli/migrate_deprecated_templates.py +28 -9
- nautobot/core/filters.py +4 -0
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/widgets.py +21 -2
- nautobot/core/jobs/bulk_actions.py +12 -6
- nautobot/core/jobs/cleanup.py +13 -1
- nautobot/core/settings.py +6 -0
- nautobot/core/settings_funcs.py +11 -1
- nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
- nautobot/core/templatetags/helpers.py +9 -7
- nautobot/core/tests/nautobot_config.py +3 -0
- nautobot/core/tests/test_jobs.py +118 -0
- nautobot/core/tests/test_templatetags_helpers.py +6 -0
- nautobot/core/tests/test_ui.py +49 -1
- nautobot/core/tests/test_utils.py +41 -1
- nautobot/core/ui/object_detail.py +7 -2
- nautobot/core/urls.py +7 -8
- nautobot/core/utils/filtering.py +11 -1
- nautobot/core/utils/lookup.py +46 -0
- nautobot/core/views/mixins.py +23 -17
- nautobot/core/views/utils.py +3 -3
- nautobot/dcim/api/serializers.py +3 -0
- nautobot/dcim/choices.py +49 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/filters/__init__.py +7 -0
- nautobot/dcim/forms.py +89 -3
- nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
- nautobot/dcim/models/device_component_templates.py +33 -1
- nautobot/dcim/models/device_components.py +21 -0
- nautobot/dcim/tables/devices.py +14 -0
- nautobot/dcim/tables/devicetypes.py +8 -1
- nautobot/dcim/templates/dcim/interface.html +8 -0
- nautobot/dcim/templates/dcim/interface_edit.html +2 -0
- nautobot/dcim/tests/test_api.py +186 -6
- nautobot/dcim/tests/test_filters.py +32 -0
- nautobot/dcim/tests/test_forms.py +110 -8
- nautobot/dcim/tests/test_graphql.py +44 -1
- nautobot/dcim/tests/test_models.py +265 -0
- nautobot/dcim/tests/test_tables.py +160 -0
- nautobot/dcim/tests/test_views.py +64 -1
- nautobot/dcim/views.py +86 -77
- nautobot/extras/forms/forms.py +3 -1
- nautobot/extras/jobs.py +48 -2
- nautobot/extras/models/models.py +19 -0
- nautobot/extras/models/relationships.py +3 -1
- nautobot/extras/templates/extras/plugin_detail.html +2 -2
- nautobot/extras/urls.py +0 -14
- nautobot/extras/views.py +1 -1
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/views.py +2 -2
- nautobot/project-static/js/forms.js +92 -14
- nautobot/virtualization/tests/test_models.py +4 -2
- nautobot/virtualization/views.py +1 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/METADATA +4 -4
- {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/RECORD +62 -59
- {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/NOTICE +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/WHEEL +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.23.dist-info}/entry_points.txt +0 -0
nautobot/apps/choices.py
CHANGED
|
@@ -14,8 +14,10 @@ from nautobot.dcim.choices import (
|
|
|
14
14
|
ConsolePortTypeChoices,
|
|
15
15
|
DeviceFaceChoices,
|
|
16
16
|
DeviceRedundancyGroupFailoverStrategyChoices,
|
|
17
|
+
InterfaceDuplexChoices,
|
|
17
18
|
InterfaceModeChoices,
|
|
18
19
|
InterfaceRedundancyGroupProtocolChoices,
|
|
20
|
+
InterfaceSpeedChoices,
|
|
19
21
|
InterfaceTypeChoices,
|
|
20
22
|
PortTypeChoices,
|
|
21
23
|
PowerFeedBreakerPoleChoices,
|
|
@@ -84,8 +86,10 @@ __all__ = (
|
|
|
84
86
|
"IPAddressRoleChoices",
|
|
85
87
|
"IPAddressTypeChoices",
|
|
86
88
|
"IPAddressVersionChoices",
|
|
89
|
+
"InterfaceDuplexChoices",
|
|
87
90
|
"InterfaceModeChoices",
|
|
88
91
|
"InterfaceRedundancyGroupProtocolChoices",
|
|
92
|
+
"InterfaceSpeedChoices",
|
|
89
93
|
"InterfaceTypeChoices",
|
|
90
94
|
"JobExecutionType",
|
|
91
95
|
"JobResultStatusChoices",
|
nautobot/apps/utils.py
CHANGED
|
@@ -26,10 +26,13 @@ from nautobot.core.utils.filtering import (
|
|
|
26
26
|
from nautobot.core.utils.git import BranchDoesNotExist, convert_git_diff_log_to_list, GitRepo, swap_status_initials
|
|
27
27
|
from nautobot.core.utils.logging import sanitize
|
|
28
28
|
from nautobot.core.utils.lookup import (
|
|
29
|
+
get_breadcrumbs_for_model,
|
|
29
30
|
get_changes_for_model,
|
|
31
|
+
get_detail_view_components_context_for_model,
|
|
30
32
|
get_filterset_for_model,
|
|
31
33
|
get_form_for_model,
|
|
32
34
|
get_model_from_name,
|
|
35
|
+
get_object_detail_content_for_model,
|
|
33
36
|
get_related_class_for_model,
|
|
34
37
|
get_related_field_for_models,
|
|
35
38
|
get_route_for_model,
|
|
@@ -37,6 +40,7 @@ from nautobot.core.utils.lookup import (
|
|
|
37
40
|
get_url_for_url_pattern,
|
|
38
41
|
get_url_patterns,
|
|
39
42
|
get_view_for_model,
|
|
43
|
+
get_view_titles_for_model,
|
|
40
44
|
)
|
|
41
45
|
from nautobot.core.utils.migrations import migrate_content_type_references_to_new_model
|
|
42
46
|
from nautobot.core.utils.permissions import (
|
|
@@ -97,8 +101,10 @@ __all__ = (
|
|
|
97
101
|
"generate_signature",
|
|
98
102
|
"get_all_lookup_expr_for_field",
|
|
99
103
|
"get_base_template",
|
|
104
|
+
"get_breadcrumbs_for_model",
|
|
100
105
|
"get_celery_queues",
|
|
101
106
|
"get_changes_for_model",
|
|
107
|
+
"get_detail_view_components_context_for_model",
|
|
102
108
|
"get_filter_field_label",
|
|
103
109
|
"get_filterable_params_from_filter_params",
|
|
104
110
|
"get_filterset_field",
|
|
@@ -107,6 +113,7 @@ __all__ = (
|
|
|
107
113
|
"get_form_for_model",
|
|
108
114
|
"get_latest_release",
|
|
109
115
|
"get_model_from_name",
|
|
116
|
+
"get_object_detail_content_for_model",
|
|
110
117
|
"get_permission_for_model",
|
|
111
118
|
"get_related_class_for_model",
|
|
112
119
|
"get_related_field_for_models",
|
|
@@ -116,6 +123,7 @@ __all__ = (
|
|
|
116
123
|
"get_url_for_url_pattern",
|
|
117
124
|
"get_url_patterns",
|
|
118
125
|
"get_view_for_model",
|
|
126
|
+
"get_view_titles_for_model",
|
|
119
127
|
"get_worker_count",
|
|
120
128
|
"hex_to_rgb",
|
|
121
129
|
"image_upload",
|
nautobot/circuits/views.py
CHANGED
|
@@ -60,7 +60,7 @@ class CircuitTypeUIViewSet(NautobotUIViewSet):
|
|
|
60
60
|
|
|
61
61
|
class CircuitTerminationObjectFieldsPanel(ObjectFieldsPanel):
|
|
62
62
|
def get_extra_context(self, context):
|
|
63
|
-
return {"termination": context["object"]}
|
|
63
|
+
return {"termination": context["object"], **super().get_extra_context(context)}
|
|
64
64
|
|
|
65
65
|
def render_key(self, key, value, context):
|
|
66
66
|
if key == "connected_endpoint":
|
|
@@ -226,7 +226,11 @@ class CircuitUIViewSet(NautobotUIViewSet):
|
|
|
226
226
|
return True
|
|
227
227
|
|
|
228
228
|
def get_extra_context(self, context):
|
|
229
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
"termination": context[self.context_object_key],
|
|
231
|
+
"side": self.side,
|
|
232
|
+
**super().get_extra_context(context),
|
|
233
|
+
}
|
|
230
234
|
|
|
231
235
|
def get_data(self, context):
|
|
232
236
|
"""
|
|
@@ -4,15 +4,16 @@ import re
|
|
|
4
4
|
|
|
5
5
|
TEMPLATE_REPLACEMENTS = {
|
|
6
6
|
# Format: new_template: [old_template1, old_template2, ...]
|
|
7
|
-
"circuits/circuit_create.html": ["circuits/circuit_edit.html"],
|
|
8
|
-
"circuits/circuittermination_create.html": [
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
"circuits/circuit_create.html": ["circuits/circuit_edit.html", "circuits/circuit_update.html"],
|
|
8
|
+
"circuits/circuittermination_create.html": [
|
|
9
|
+
"circuits/circuittermination_edit.html",
|
|
10
|
+
"circuits/circuittermination_update.html",
|
|
11
|
+
],
|
|
12
|
+
"circuits/provider_create.html": ["circuits/provider_edit.html", "circuits/provider_update.html"],
|
|
11
13
|
"dcim/cable_retrieve.html": ["dcim/cable.html"],
|
|
12
14
|
"dcim/cable_update.html": ["dcim/cable_edit.html"],
|
|
13
15
|
"dcim/device_create.html": ["dcim/device_edit.html"],
|
|
14
16
|
"dcim/devicetype_update.html": ["dcim/devicetype_edit.html"],
|
|
15
|
-
"dcim/location_retrieve.html": ["dcim/location.html"],
|
|
16
17
|
"dcim/location_update.html": ["dcim/location_edit.html"],
|
|
17
18
|
"dcim/rack_retrieve.html": ["dcim/rack.html"],
|
|
18
19
|
"dcim/rack_update.html": ["dcim/rack_edit.html"],
|
|
@@ -26,10 +27,9 @@ TEMPLATE_REPLACEMENTS = {
|
|
|
26
27
|
"extras/dynamicgroup_update.html": ["extras/dynamicgroup_edit.html"],
|
|
27
28
|
"extras/gitrepository_retrieve.html": ["extras/gitrepository.html"],
|
|
28
29
|
"extras/gitrepository_update.html": ["extras/gitrepository_object_edit.html"],
|
|
29
|
-
"extras/graphqlquery_retrieve.html": ["extras/graphqlquery.html"],
|
|
30
30
|
"extras/jobresult_retrieve.html": ["extras/jobresult.html"],
|
|
31
|
-
"extras/note_retrieve.html": ["extras/note.html"],
|
|
32
31
|
"extras/objectchange_retrieve.html": ["extras/objectchange.html"],
|
|
32
|
+
"extras/secret_create.html": ["extras/secret_edit.html"],
|
|
33
33
|
"extras/secretsgroup_update.html": ["extras/secretsgroup_edit.html"],
|
|
34
34
|
"extras/tag_update.html": ["extras/tag_edit.html"],
|
|
35
35
|
"generic/object_bulk_create.html": ["generic/object_bulk_import.html"],
|
|
@@ -38,6 +38,7 @@ TEMPLATE_REPLACEMENTS = {
|
|
|
38
38
|
"generic/object_changelog.html": ["extras/object_changelog.html"],
|
|
39
39
|
"generic/object_create.html": ["dcim/powerpanel_edit.html", "generic/object_edit.html", "ipam/service_edit.html"],
|
|
40
40
|
"generic/object_destroy.html": ["generic/object_delete.html"],
|
|
41
|
+
"generic/object_list.html": ["extras/graphqlquery_list.html", "extras/objectchange_list.html"],
|
|
41
42
|
"generic/object_notes.html": ["extras/object_notes.html"],
|
|
42
43
|
"generic/object_retrieve.html": [
|
|
43
44
|
"circuits/circuit.html",
|
|
@@ -46,6 +47,8 @@ TEMPLATE_REPLACEMENTS = {
|
|
|
46
47
|
"circuits/circuittermination_retrieve.html",
|
|
47
48
|
"circuits/circuittype.html",
|
|
48
49
|
"circuits/circuittype_retrieve.html",
|
|
50
|
+
"circuits/provider.html",
|
|
51
|
+
"circuits/provider_retrieve.html",
|
|
49
52
|
"circuits/providernetwork.html",
|
|
50
53
|
"circuits/providernetwork_retrieve.html",
|
|
51
54
|
"cloud/cloudaccount_retrieve.html",
|
|
@@ -68,14 +71,18 @@ TEMPLATE_REPLACEMENTS = {
|
|
|
68
71
|
"dcim/device/powerports.html",
|
|
69
72
|
"dcim/device/rearports.html",
|
|
70
73
|
"dcim/device/wireless.html",
|
|
74
|
+
"dcim/device_component.html",
|
|
71
75
|
"dcim/devicefamily_retrieve.html",
|
|
72
76
|
"dcim/deviceredundancygroup_retrieve.html",
|
|
73
77
|
"dcim/devicetype.html",
|
|
74
78
|
"dcim/devicetype_retrieve.html",
|
|
75
79
|
"dcim/interfaceredundancygroup_retrieve.html",
|
|
80
|
+
"dcim/location.html",
|
|
81
|
+
"dcim/location_retrieve.html",
|
|
76
82
|
"dcim/locationtype.html",
|
|
77
83
|
"dcim/locationtype_retrieve.html",
|
|
78
84
|
"dcim/manufacturer.html",
|
|
85
|
+
"dcim/modulebay_retrieve.html",
|
|
79
86
|
"dcim/platform.html",
|
|
80
87
|
"dcim/powerfeed.html",
|
|
81
88
|
"dcim/powerfeed_retrieve.html",
|
|
@@ -96,11 +103,17 @@ TEMPLATE_REPLACEMENTS = {
|
|
|
96
103
|
"extras/customfield_retrieve.html",
|
|
97
104
|
"extras/customlink.html",
|
|
98
105
|
"extras/exporttemplate.html",
|
|
106
|
+
"extras/graphqlquery.html",
|
|
107
|
+
"extras/graphqlquery_retrieve.html",
|
|
99
108
|
"extras/job_detail.html",
|
|
100
109
|
"extras/jobbutton_retrieve.html",
|
|
101
110
|
"extras/jobhook.html",
|
|
102
111
|
"extras/jobqueue_retrieve.html",
|
|
103
112
|
"extras/metadatatype_retrieve.html",
|
|
113
|
+
"extras/note.html",
|
|
114
|
+
"extras/note_retrieve.html",
|
|
115
|
+
"extras/relationship.html",
|
|
116
|
+
"extras/secret.html",
|
|
104
117
|
"extras/secretsgroup.html",
|
|
105
118
|
"extras/secretsgroup_retrieve.html",
|
|
106
119
|
"extras/status.html",
|
|
@@ -108,13 +121,20 @@ TEMPLATE_REPLACEMENTS = {
|
|
|
108
121
|
"extras/tag_retrieve.html",
|
|
109
122
|
"extras/team_retrieve.html",
|
|
110
123
|
"generic/object_detail.html",
|
|
124
|
+
"ipam/namespace_retrieve.html",
|
|
125
|
+
"ipam/prefix.html",
|
|
126
|
+
"ipam/prefix_retrieve.html",
|
|
111
127
|
"ipam/rir.html",
|
|
128
|
+
"ipam/routetarget.html",
|
|
112
129
|
"ipam/service.html",
|
|
113
130
|
"ipam/service_retrieve.html",
|
|
114
131
|
"ipam/vlan.html",
|
|
115
132
|
"ipam/vlan_retrieve.html",
|
|
116
133
|
"ipam/vlangroup.html",
|
|
134
|
+
"ipam/vrf.html",
|
|
117
135
|
"tenancy/tenant.html",
|
|
136
|
+
"tenancy/tenantgroup.html",
|
|
137
|
+
"tenancy/tenantgroup_retrieve.html",
|
|
118
138
|
"virtualization/clustergroup.html",
|
|
119
139
|
"virtualization/clustertype.html",
|
|
120
140
|
"virtualization/virtualmachine.html",
|
|
@@ -123,10 +143,9 @@ TEMPLATE_REPLACEMENTS = {
|
|
|
123
143
|
"wireless/supporteddatarate_retrieve.html",
|
|
124
144
|
"wireless/wirelessnetwork_retrieve.html",
|
|
125
145
|
],
|
|
126
|
-
"ipam/
|
|
146
|
+
"ipam/prefix_create.html": ["ipam/prefix_edit.html"],
|
|
127
147
|
"ipam/vlan_update.html": ["ipam/vlan_edit.html"],
|
|
128
148
|
"tenancy/tenant_create.html": ["tenancy/tenant_edit.html"],
|
|
129
|
-
"tenancy/tenantgroup_retrieve.html": ["tenancy/tenantgroup.html"],
|
|
130
149
|
"virtualchassis_update.html": ["dcim/virtualchassis_edit.html"],
|
|
131
150
|
"virtualization/virtualmachine_update.html": ["virtualization/virtualmachine_edit.html"],
|
|
132
151
|
}
|
nautobot/core/filters.py
CHANGED
|
@@ -91,6 +91,10 @@ class MultiValueDateTimeFilter(django_filters.DateTimeFilter, django_filters.Mul
|
|
|
91
91
|
class MultiValueNumberFilter(django_filters.NumberFilter, django_filters.MultipleChoiceFilter):
|
|
92
92
|
field_class = multivalue_field_factory(django_forms.IntegerField)
|
|
93
93
|
|
|
94
|
+
def __init__(self, *args, choices=None, **kwargs):
|
|
95
|
+
super().__init__(*args, **kwargs)
|
|
96
|
+
self.choices = list(choices) if choices is not None else None
|
|
97
|
+
|
|
94
98
|
|
|
95
99
|
class MultiValueBigNumberFilter(MultiValueNumberFilter):
|
|
96
100
|
"""Subclass of MultiValueNumberFilter used for BigInteger model fields."""
|
nautobot/core/forms/__init__.py
CHANGED
|
@@ -68,6 +68,7 @@ from nautobot.core.forms.widgets import (
|
|
|
68
68
|
DatePicker,
|
|
69
69
|
DateTimePicker,
|
|
70
70
|
MultiValueCharInput,
|
|
71
|
+
NumberWithSelect,
|
|
71
72
|
SelectWithDisabled,
|
|
72
73
|
SelectWithPK,
|
|
73
74
|
SlugWidget,
|
|
@@ -124,6 +125,7 @@ __all__ = (
|
|
|
124
125
|
"MultiValueCharInput",
|
|
125
126
|
"MultipleContentTypeField",
|
|
126
127
|
"NullableDateField",
|
|
128
|
+
"NumberWithSelect",
|
|
127
129
|
"NumericArrayField",
|
|
128
130
|
"PrefixFieldMixin",
|
|
129
131
|
"ReturnURLForm",
|
nautobot/core/forms/widgets.py
CHANGED
|
@@ -6,7 +6,7 @@ from django import forms
|
|
|
6
6
|
from django.forms.models import ModelChoiceIterator
|
|
7
7
|
from django.urls import get_script_prefix
|
|
8
8
|
|
|
9
|
-
from nautobot.core import choices
|
|
9
|
+
from nautobot.core import choices as core_choices
|
|
10
10
|
from nautobot.core.forms import utils
|
|
11
11
|
|
|
12
12
|
__all__ = (
|
|
@@ -19,6 +19,7 @@ __all__ = (
|
|
|
19
19
|
"ContentTypeSelect",
|
|
20
20
|
"DatePicker",
|
|
21
21
|
"DateTimePicker",
|
|
22
|
+
"NumberWithSelect",
|
|
22
23
|
"SelectWithDisabled",
|
|
23
24
|
"SelectWithPK",
|
|
24
25
|
"SlugWidget",
|
|
@@ -68,7 +69,7 @@ class ColorSelect(forms.Select):
|
|
|
68
69
|
option_template_name = "widgets/colorselect_option.html"
|
|
69
70
|
|
|
70
71
|
def __init__(self, *args, **kwargs):
|
|
71
|
-
kwargs["choices"] = utils.add_blank_choice(
|
|
72
|
+
kwargs["choices"] = utils.add_blank_choice(core_choices.ColorChoices)
|
|
72
73
|
super().__init__(*args, **kwargs)
|
|
73
74
|
self.attrs["class"] = "nautobot-select2-color-picker"
|
|
74
75
|
|
|
@@ -282,3 +283,21 @@ class ClearableFileInput(forms.ClearableFileInput):
|
|
|
282
283
|
|
|
283
284
|
class Media:
|
|
284
285
|
js = ["bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js"]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class NumberWithSelect(forms.NumberInput):
|
|
289
|
+
template_name = "widgets/number_input_with_choices.html"
|
|
290
|
+
|
|
291
|
+
def __init__(self, choices=None, *args, **kwargs):
|
|
292
|
+
super().__init__(*args, **kwargs)
|
|
293
|
+
if choices is None:
|
|
294
|
+
self.choices = []
|
|
295
|
+
elif hasattr(choices, "CHOICES"):
|
|
296
|
+
self.choices = core_choices.unpack_grouped_choices(choices.CHOICES)
|
|
297
|
+
else:
|
|
298
|
+
self.choices = core_choices.unpack_grouped_choices(choices)
|
|
299
|
+
|
|
300
|
+
def get_context(self, name, value, attrs):
|
|
301
|
+
context = super().get_context(name, value, attrs)
|
|
302
|
+
context["widget"]["choices"] = self.choices
|
|
303
|
+
return context
|
|
@@ -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.py
CHANGED
|
@@ -822,6 +822,11 @@ CONSTANCE_CONFIG = {
|
|
|
822
822
|
help_text="Whether to prefer IPv4 primary addresses over IPv6 primary addresses for devices.",
|
|
823
823
|
field_type=bool,
|
|
824
824
|
),
|
|
825
|
+
"RACK_DEFAULT_U_HEIGHT": ConstanceConfigItem(
|
|
826
|
+
default=42,
|
|
827
|
+
help_text="Default height in rack units (U) for newly created racks. Must be between 1 and 500.",
|
|
828
|
+
field_type=int,
|
|
829
|
+
),
|
|
825
830
|
"RACK_ELEVATION_DEFAULT_UNIT_HEIGHT": ConstanceConfigItem(
|
|
826
831
|
default=22, help_text="Default height (in pixels) of a rack unit in a rack elevation diagram", field_type=int
|
|
827
832
|
),
|
|
@@ -865,6 +870,7 @@ CONSTANCE_CONFIG_FIELDSETS = {
|
|
|
865
870
|
"Pagination": ["PAGINATE_COUNT", "MAX_PAGE_SIZE", "PER_PAGE_DEFAULTS"],
|
|
866
871
|
"Performance": ["JOB_CREATE_FILE_MAX_SIZE"],
|
|
867
872
|
"Rack Elevation Rendering": [
|
|
873
|
+
"RACK_DEFAULT_U_HEIGHT",
|
|
868
874
|
"RACK_ELEVATION_DEFAULT_UNIT_HEIGHT",
|
|
869
875
|
"RACK_ELEVATION_DEFAULT_UNIT_WIDTH",
|
|
870
876
|
"RACK_ELEVATION_UNIT_TWO_DIGIT_FORMAT",
|
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.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<div class="input-group">
|
|
2
|
+
{% include 'django/forms/widgets/number.html' %}
|
|
3
|
+
{% if widget.choices %}
|
|
4
|
+
<span class="input-group-btn">
|
|
5
|
+
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
|
6
|
+
<span class="caret"></span>
|
|
7
|
+
</button>
|
|
8
|
+
<ul class="dropdown-menu dropdown-menu-right">
|
|
9
|
+
{% for value, label in widget.choices %}
|
|
10
|
+
<li><a href="#" data-name="{{ widget.name }}" data-value="{{ value }}" class="set_value">{{ label }}</a></li>
|
|
11
|
+
{% endfor %}
|
|
12
|
+
</ul>
|
|
13
|
+
</span>
|
|
14
|
+
{% endif %}
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
{% if widget.choices %}
|
|
18
|
+
<script type="text/javascript">
|
|
19
|
+
(function() {
|
|
20
|
+
if (window.__nbNumberWithSelectWidgetBound) return;
|
|
21
|
+
window.__nbNumberWithSelectWidgetBound = true;
|
|
22
|
+
function bindNumberWithSelectHandler() {
|
|
23
|
+
document.addEventListener("click", function(e) {
|
|
24
|
+
if (!e.target) return;
|
|
25
|
+
var link = e.target.closest && e.target.closest("a.set_value");
|
|
26
|
+
if (!link) return;
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
var container = link.closest(".input-group");
|
|
29
|
+
var name = link.getAttribute("data-name");
|
|
30
|
+
var value = link.getAttribute("data-value");
|
|
31
|
+
var input = container && name ? container.querySelector('input[name="' + name + '"]') : null;
|
|
32
|
+
if (input) {
|
|
33
|
+
input.value = value;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (document.readyState === 'loading') {
|
|
38
|
+
document.addEventListener('DOMContentLoaded', bindNumberWithSelectHandler);
|
|
39
|
+
} else {
|
|
40
|
+
bindNumberWithSelectHandler();
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
</script>
|
|
44
|
+
{% endif %}
|
|
@@ -393,17 +393,19 @@ def humanize_speed(speed):
|
|
|
393
393
|
1544 => "1.544 Mbps"
|
|
394
394
|
100000 => "100 Mbps"
|
|
395
395
|
10000000 => "10 Gbps"
|
|
396
|
+
1000000000 => "1 Tbps"
|
|
397
|
+
1600000000 => "1.6 Tbps"
|
|
398
|
+
10000000000 => "10 Tbps"
|
|
396
399
|
"""
|
|
397
400
|
if not speed:
|
|
398
401
|
return ""
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return f"{int(speed / 1000)} Mbps"
|
|
402
|
+
|
|
403
|
+
if speed >= 1000000000:
|
|
404
|
+
return f"{speed / 1000000000:g} Tbps"
|
|
405
|
+
elif speed >= 1000000:
|
|
406
|
+
return f"{speed / 1000000:g} Gbps"
|
|
405
407
|
elif speed >= 1000:
|
|
406
|
-
return f"{
|
|
408
|
+
return f"{speed / 1000:g} Mbps"
|
|
407
409
|
else:
|
|
408
410
|
return f"{speed} Kbps"
|
|
409
411
|
|
|
@@ -10,6 +10,9 @@ from nautobot.core.settings_funcs import parse_redis_connection
|
|
|
10
10
|
|
|
11
11
|
ALLOWED_HOSTS = ["nautobot.example.com"]
|
|
12
12
|
|
|
13
|
+
# Do *not* send anonymized install metrics when migration or post_upgrade management commands are run while testing
|
|
14
|
+
INSTALLATION_METRICS_ENABLED = False
|
|
15
|
+
|
|
13
16
|
# Discover test jobs from within the Nautobot source code
|
|
14
17
|
JOBS_ROOT = os.path.join(
|
|
15
18
|
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "extras", "test_jobs"
|
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):
|
|
@@ -180,7 +180,13 @@ class NautobotTemplatetagsHelperTest(TestCase):
|
|
|
180
180
|
def test_humanize_speed(self):
|
|
181
181
|
self.assertEqual(helpers.humanize_speed(1544), "1.544 Mbps")
|
|
182
182
|
self.assertEqual(helpers.humanize_speed(100000), "100 Mbps")
|
|
183
|
+
self.assertEqual(helpers.humanize_speed(2500000), "2.5 Gbps")
|
|
183
184
|
self.assertEqual(helpers.humanize_speed(10000000), "10 Gbps")
|
|
185
|
+
self.assertEqual(helpers.humanize_speed(100000000), "100 Gbps")
|
|
186
|
+
self.assertEqual(helpers.humanize_speed(1000000000), "1 Tbps")
|
|
187
|
+
self.assertEqual(helpers.humanize_speed(1600000000), "1.6 Tbps")
|
|
188
|
+
self.assertEqual(helpers.humanize_speed(10000000000), "10 Tbps")
|
|
189
|
+
self.assertEqual(helpers.humanize_speed(100000000000), "100 Tbps")
|
|
184
190
|
|
|
185
191
|
def test_tzoffset(self):
|
|
186
192
|
self.assertTrue(callable(helpers.tzoffset))
|