nautobot 2.4.21__py3-none-any.whl → 2.4.22__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.
- 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/settings.py +6 -0
- 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_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 +21 -16
- 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/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.22.dist-info}/METADATA +4 -4
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/RECORD +54 -51
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/NOTICE +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/WHEEL +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.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
|
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",
|
|
@@ -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"
|
|
@@ -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))
|
nautobot/core/tests/test_ui.py
CHANGED
|
@@ -23,8 +23,10 @@ from nautobot.core.ui.object_detail import (
|
|
|
23
23
|
Panel,
|
|
24
24
|
SectionChoices,
|
|
25
25
|
)
|
|
26
|
-
from nautobot.dcim.models import DeviceRedundancyGroup
|
|
26
|
+
from nautobot.dcim.models import Device, DeviceRedundancyGroup
|
|
27
|
+
from nautobot.dcim.tables import DeviceModuleInterfaceTable
|
|
27
28
|
from nautobot.dcim.tables.devices import DeviceTable
|
|
29
|
+
from nautobot.dcim.views import DeviceUIViewSet
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class DataTablePanelTest(TestCase):
|
|
@@ -288,6 +290,52 @@ class ObjectDetailContentExtraTabsTest(TestCase):
|
|
|
288
290
|
self.default_tabs_id.append("services")
|
|
289
291
|
self.assertListEqual(tab_ids, self.default_tabs_id)
|
|
290
292
|
|
|
293
|
+
def test_tab_id_url_as_action(self):
|
|
294
|
+
"""
|
|
295
|
+
Test that when you create a panel with a tab_id that matches a viewset action,
|
|
296
|
+
the return_url is constructed correctly.
|
|
297
|
+
"""
|
|
298
|
+
self.add_permissions("dcim.add_interface", "dcim.change_interface")
|
|
299
|
+
device_info = Device.objects.first()
|
|
300
|
+
|
|
301
|
+
panel = DeviceUIViewSet.DeviceInterfacesTablePanel(
|
|
302
|
+
weight=100,
|
|
303
|
+
section=SectionChoices.FULL_WIDTH,
|
|
304
|
+
table_title="Interfaces",
|
|
305
|
+
table_class=DeviceModuleInterfaceTable,
|
|
306
|
+
table_attribute="vc_interfaces",
|
|
307
|
+
related_field_name="device",
|
|
308
|
+
tab_id="interfaces",
|
|
309
|
+
)
|
|
310
|
+
context = {"request": self.request, "object": device_info}
|
|
311
|
+
panel_context = panel.get_extra_context(context)
|
|
312
|
+
|
|
313
|
+
return_url = f"/dcim/devices/{device_info.pk}/interfaces/"
|
|
314
|
+
self.assertTrue(panel_context["body_content_table_add_url"].endswith(return_url))
|
|
315
|
+
|
|
316
|
+
def test_tab_id_url_as_param(self):
|
|
317
|
+
"""
|
|
318
|
+
Test that when you create a panel with a tab_id that does NOT matches a viewset action,
|
|
319
|
+
the return_url is constructed correctly.
|
|
320
|
+
"""
|
|
321
|
+
self.add_permissions("dcim.add_interface", "dcim.change_interface")
|
|
322
|
+
device_info = Device.objects.first()
|
|
323
|
+
|
|
324
|
+
panel = DeviceUIViewSet.DeviceInterfacesTablePanel(
|
|
325
|
+
weight=100,
|
|
326
|
+
section=SectionChoices.FULL_WIDTH,
|
|
327
|
+
table_title="Interfaces",
|
|
328
|
+
table_class=DeviceModuleInterfaceTable,
|
|
329
|
+
table_attribute="vc_interfaces",
|
|
330
|
+
related_field_name="device",
|
|
331
|
+
tab_id="interfaces-not-exist",
|
|
332
|
+
)
|
|
333
|
+
context = {"request": self.request, "object": device_info}
|
|
334
|
+
panel_context = panel.get_extra_context(context)
|
|
335
|
+
|
|
336
|
+
return_url = f"&return_url=/dcim/devices/{device_info.pk}/?tab=interfaces-not-exist"
|
|
337
|
+
self.assertTrue(panel_context["body_content_table_add_url"].endswith(return_url))
|
|
338
|
+
|
|
291
339
|
def test_extra_tab_panel_context(self):
|
|
292
340
|
"""
|
|
293
341
|
Confirming that extra tab panels produce the correct context,
|
|
@@ -18,7 +18,13 @@ from nautobot.core.testing import TestCase
|
|
|
18
18
|
from nautobot.core.utils import data as data_utils, filtering, lookup, querysets, requests
|
|
19
19
|
from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
|
|
20
20
|
from nautobot.core.utils.module_loading import check_name_safe_to_import_privately
|
|
21
|
-
from nautobot.dcim import
|
|
21
|
+
from nautobot.dcim import (
|
|
22
|
+
filters as dcim_filters,
|
|
23
|
+
forms as dcim_forms,
|
|
24
|
+
models as dcim_models,
|
|
25
|
+
tables,
|
|
26
|
+
views as dcim_views,
|
|
27
|
+
)
|
|
22
28
|
from nautobot.extras import models as extras_models, utils as extras_utils
|
|
23
29
|
from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
|
|
24
30
|
from nautobot.extras.filters import StatusFilterSet
|
|
@@ -213,6 +219,26 @@ class FlattenIterableTest(TestCase):
|
|
|
213
219
|
class GetFooForModelTest(TestCase):
|
|
214
220
|
"""Tests for the various `get_foo_for_model()` functions."""
|
|
215
221
|
|
|
222
|
+
def test_get_breadcrumbs_for_model(self):
|
|
223
|
+
breadcrumbs = lookup.get_breadcrumbs_for_model(dcim_models.Device)
|
|
224
|
+
self.assertEqual(breadcrumbs.items, dcim_views.DeviceUIViewSet.get_breadcrumbs(dcim_models.Device).items)
|
|
225
|
+
breadcrumbs = lookup.get_breadcrumbs_for_model(dcim_models.Device, view_type="")
|
|
226
|
+
self.assertEqual(
|
|
227
|
+
breadcrumbs.items, dcim_views.DeviceUIViewSet.get_breadcrumbs(dcim_models.Device, view_type="").items
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def test_get_detail_view_components_context_for_model(self):
|
|
231
|
+
context = lookup.get_detail_view_components_context_for_model(dcim_models.Device)
|
|
232
|
+
self.assertEqual(
|
|
233
|
+
context["breadcrumbs"].items, lookup.get_breadcrumbs_for_model(dcim_models.Device, view_type="").items
|
|
234
|
+
)
|
|
235
|
+
self.assertEqual(
|
|
236
|
+
context["object_detail_content"], lookup.get_object_detail_content_for_model(dcim_models.Device)
|
|
237
|
+
)
|
|
238
|
+
self.assertEqual(
|
|
239
|
+
context["view_titles"].titles, lookup.get_view_titles_for_model(dcim_models.Device, view_type="").titles
|
|
240
|
+
)
|
|
241
|
+
|
|
216
242
|
def test_get_filterset_for_model(self):
|
|
217
243
|
"""
|
|
218
244
|
Test that `get_filterset_for_model` returns the right FilterSet for various inputs.
|
|
@@ -235,6 +261,12 @@ class GetFooForModelTest(TestCase):
|
|
|
235
261
|
self.assertEqual(lookup.get_form_for_model("dcim.location"), dcim_forms.LocationForm)
|
|
236
262
|
self.assertEqual(lookup.get_form_for_model(dcim_models.Location), dcim_forms.LocationForm)
|
|
237
263
|
|
|
264
|
+
def test_get_object_detail_content_for_model(self):
|
|
265
|
+
self.assertEqual(
|
|
266
|
+
lookup.get_object_detail_content_for_model(dcim_models.Device),
|
|
267
|
+
dcim_views.DeviceUIViewSet.object_detail_content,
|
|
268
|
+
)
|
|
269
|
+
|
|
238
270
|
def test_get_related_field_for_models(self):
|
|
239
271
|
"""
|
|
240
272
|
Test that `get_related_field_for_models` returns the appropriate field for various inputs.
|
|
@@ -341,6 +373,14 @@ class GetFooForModelTest(TestCase):
|
|
|
341
373
|
# Testing unconventional table name
|
|
342
374
|
self.assertEqual(lookup.get_table_class_string_from_view_name("ipam:prefix_list"), "PrefixDetailTable")
|
|
343
375
|
|
|
376
|
+
def test_get_view_titles_for_model(self):
|
|
377
|
+
view_titles = lookup.get_view_titles_for_model(dcim_models.Device)
|
|
378
|
+
self.assertEqual(view_titles.titles, dcim_views.DeviceUIViewSet.get_view_titles(dcim_models.Device).titles)
|
|
379
|
+
view_titles = lookup.get_view_titles_for_model(dcim_models.Device, view_type="")
|
|
380
|
+
self.assertEqual(
|
|
381
|
+
view_titles.titles, dcim_views.DeviceUIViewSet.get_view_titles(dcim_models.Device, view_type="").titles
|
|
382
|
+
)
|
|
383
|
+
|
|
344
384
|
|
|
345
385
|
class IsTaggableTest(TestCase):
|
|
346
386
|
def test_is_taggable_true(self):
|
|
@@ -39,7 +39,7 @@ from nautobot.core.templatetags.helpers import (
|
|
|
39
39
|
)
|
|
40
40
|
from nautobot.core.ui.choices import LayoutChoices, SectionChoices
|
|
41
41
|
from nautobot.core.ui.utils import render_component_template
|
|
42
|
-
from nautobot.core.utils.lookup import get_filterset_for_model, get_route_for_model
|
|
42
|
+
from nautobot.core.utils.lookup import get_filterset_for_model, get_route_for_model, get_view_for_model
|
|
43
43
|
from nautobot.core.utils.permissions import get_permission_for_model
|
|
44
44
|
from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
|
|
45
45
|
from nautobot.core.views.utils import get_obj_from_context
|
|
@@ -879,7 +879,12 @@ class ObjectsTablePanel(Panel):
|
|
|
879
879
|
related_field_name = self.related_field_name or self.table_filter or obj._meta.model_name
|
|
880
880
|
return_url = context.get("return_url", obj.get_absolute_url())
|
|
881
881
|
if self.tab_id:
|
|
882
|
-
|
|
882
|
+
try:
|
|
883
|
+
# Check to see if the this is a NautobotUIViewset action
|
|
884
|
+
view = get_view_for_model(obj._meta.model)
|
|
885
|
+
return_url += getattr(view, self.tab_id).url_path + "/"
|
|
886
|
+
except AttributeError:
|
|
887
|
+
return_url += f"?tab={self.tab_id}"
|
|
883
888
|
|
|
884
889
|
if self.add_button_route is not None:
|
|
885
890
|
add_permissions = self.add_permissions
|
nautobot/core/urls.py
CHANGED
|
@@ -91,15 +91,14 @@ urlpatterns = [
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
if settings.DEBUG:
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
urlpatterns += [path("theme-preview/", ThemePreviewView.as_view(), name="theme_preview")]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if "debug_toolbar" in settings.INSTALLED_APPS:
|
|
98
|
+
from debug_toolbar.toolbar import debug_toolbar_urls
|
|
99
|
+
|
|
100
|
+
urlpatterns += debug_toolbar_urls()
|
|
96
101
|
|
|
97
|
-
urlpatterns += [
|
|
98
|
-
path("__debug__/", include(debug_toolbar.urls)),
|
|
99
|
-
path("theme-preview/", ThemePreviewView.as_view(), name="theme_preview"),
|
|
100
|
-
]
|
|
101
|
-
except ImportError:
|
|
102
|
-
pass
|
|
103
102
|
|
|
104
103
|
if settings.METRICS_ENABLED:
|
|
105
104
|
if settings.METRICS_AUTHENTICATED:
|
nautobot/core/utils/filtering.py
CHANGED
|
@@ -101,6 +101,7 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
|
|
|
101
101
|
BOOLEAN_CHOICES,
|
|
102
102
|
DynamicModelMultipleChoiceField,
|
|
103
103
|
MultipleContentTypeField,
|
|
104
|
+
MultiValueCharInput,
|
|
104
105
|
StaticSelect2,
|
|
105
106
|
StaticSelect2Multiple,
|
|
106
107
|
)
|
|
@@ -121,7 +122,16 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
|
|
|
121
122
|
elif isinstance(field, (MultiValueDecimalFilter, MultiValueFloatFilter)):
|
|
122
123
|
form_field = forms.DecimalField()
|
|
123
124
|
elif isinstance(field, NumberFilter):
|
|
124
|
-
|
|
125
|
+
# If "choices" are passed, then when 'exact' is used in an Advanced
|
|
126
|
+
# Filter, render a dropdown of choices instead of a free integer input
|
|
127
|
+
if field.lookup_expr == "exact" and getattr(field, "choices", None):
|
|
128
|
+
# Use a multi-value widget that allows both preset choices and free-form entries
|
|
129
|
+
form_field = forms.MultipleChoiceField(
|
|
130
|
+
choices=field.choices,
|
|
131
|
+
widget=MultiValueCharInput,
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
form_field = forms.IntegerField()
|
|
125
135
|
elif isinstance(field, ModelMultipleChoiceFilter):
|
|
126
136
|
if getattr(field, "prefers_id", False):
|
|
127
137
|
to_field_name = "id"
|