nautobot 2.4.5__py3-none-any.whl → 2.4.6__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/core/api/mixins.py +10 -0
- nautobot/core/celery/encoders.py +2 -2
- nautobot/core/forms/fields.py +21 -5
- nautobot/core/forms/utils.py +1 -0
- nautobot/core/jobs/bulk_actions.py +1 -1
- nautobot/core/management/commands/generate_test_data.py +1 -1
- nautobot/core/models/name_color_content_types.py +9 -0
- nautobot/core/models/validators.py +7 -0
- nautobot/core/settings.py +0 -14
- nautobot/core/settings.yaml +0 -28
- nautobot/core/tables.py +6 -1
- nautobot/core/templates/generic/object_retrieve.html +1 -1
- nautobot/core/testing/api.py +18 -0
- nautobot/core/tests/nautobot_config.py +0 -2
- nautobot/core/tests/runner.py +17 -140
- nautobot/core/tests/test_api.py +4 -4
- nautobot/core/tests/test_authentication.py +83 -4
- nautobot/core/tests/test_forms.py +11 -8
- nautobot/core/tests/test_graphql.py +9 -0
- nautobot/core/tests/test_jobs.py +7 -0
- nautobot/core/ui/object_detail.py +31 -0
- nautobot/dcim/factory.py +2 -0
- nautobot/dcim/filters/__init__.py +5 -0
- nautobot/dcim/forms.py +17 -1
- nautobot/dcim/migrations/0068_alter_softwareimagefile_download_url.py +19 -0
- nautobot/dcim/migrations/0069_softwareimagefile_external_integration.py +25 -0
- nautobot/dcim/models/devices.py +9 -2
- nautobot/dcim/tables/devices.py +1 -0
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +4 -0
- nautobot/dcim/tests/test_api.py +74 -31
- nautobot/dcim/tests/test_filters.py +2 -0
- nautobot/dcim/tests/test_models.py +65 -0
- nautobot/dcim/tests/test_views.py +3 -0
- nautobot/extras/forms/forms.py +7 -3
- nautobot/extras/plugins/marketplace_manifest.yml +18 -0
- nautobot/extras/tables.py +4 -5
- nautobot/extras/templates/extras/inc/panel_changelog.html +1 -1
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
- nautobot/extras/templates/extras/status.html +1 -37
- nautobot/extras/tests/integration/test_notes.py +1 -1
- nautobot/extras/tests/test_api.py +22 -7
- nautobot/extras/tests/test_changelog.py +4 -4
- nautobot/extras/tests/test_customfields.py +3 -0
- nautobot/extras/tests/test_plugins.py +19 -13
- nautobot/extras/tests/test_relationships.py +9 -0
- nautobot/extras/tests/test_tags.py +2 -2
- nautobot/extras/tests/test_views.py +15 -6
- nautobot/extras/urls.py +1 -30
- nautobot/extras/views.py +10 -54
- nautobot/ipam/tables.py +6 -2
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -41
- nautobot/ipam/templates/ipam/service.html +2 -46
- nautobot/ipam/templates/ipam/service_edit.html +1 -17
- nautobot/ipam/templates/ipam/service_retrieve.html +7 -0
- nautobot/ipam/tests/migration/__init__.py +0 -0
- nautobot/ipam/tests/migration/test_migrations.py +510 -0
- nautobot/ipam/tests/test_api.py +66 -36
- nautobot/ipam/tests/test_filters.py +0 -10
- nautobot/ipam/tests/test_views.py +44 -2
- nautobot/ipam/urls.py +2 -47
- nautobot/ipam/utils/migrations.py +185 -152
- nautobot/ipam/utils/testing.py +177 -0
- nautobot/ipam/views.py +95 -157
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +47 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +63 -0
- nautobot/project-static/docs/development/apps/api/testing.html +0 -87
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
- nautobot/project-static/docs/development/core/best-practices.html +3 -3
- nautobot/project-static/docs/development/core/getting-started.html +78 -107
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +1 -1
- nautobot/project-static/docs/development/core/testing.html +24 -198
- nautobot/project-static/docs/media/user-guide/administration/getting-started/nautobot-cloud.png +0 -0
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.4.html +226 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -48
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +71 -0
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -1
- nautobot/project-static/docs/user-guide/administration/installation/index.html +257 -16
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -0
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +8 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +40 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +77 -5
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +0 -1
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- nautobot/project-static/docs/user-guide/index.html +89 -2
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +207 -122
- nautobot/virtualization/forms.py +20 -0
- nautobot/virtualization/templates/virtualization/clustergroup.html +1 -39
- nautobot/virtualization/templates/virtualization/clustertype.html +1 -0
- nautobot/virtualization/tests/test_api.py +14 -3
- nautobot/virtualization/tests/test_views.py +10 -2
- nautobot/virtualization/urls.py +10 -93
- nautobot/virtualization/views.py +33 -72
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/METADATA +6 -5
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/RECORD +113 -108
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/WHEEL +1 -1
- nautobot/core/tests/performance_baselines.yml +0 -8900
- nautobot/ipam/tests/test_migrations.py +0 -462
- /nautobot/ipam/templates/ipam/{namespace_ipaddresses.html → namespace_ip_addresses.html} +0 -0
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/NOTICE +0 -0
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/entry_points.txt +0 -0
nautobot/core/api/mixins.py
CHANGED
|
@@ -109,6 +109,16 @@ class WritableSerializerMixin:
|
|
|
109
109
|
queryset = self.queryset
|
|
110
110
|
else:
|
|
111
111
|
queryset = self.Meta.model.objects
|
|
112
|
+
|
|
113
|
+
# Apply user permission on related objects
|
|
114
|
+
if (
|
|
115
|
+
"request" in self.context
|
|
116
|
+
and self.context["request"]
|
|
117
|
+
and self.context["request"].user
|
|
118
|
+
and hasattr(queryset, "restrict")
|
|
119
|
+
):
|
|
120
|
+
queryset = queryset.restrict(self.context["request"].user, "view")
|
|
121
|
+
|
|
112
122
|
if isinstance(data, list):
|
|
113
123
|
return [self.get_object(data=entry, queryset=queryset) for entry in data]
|
|
114
124
|
return self.get_object(data=data, queryset=queryset)
|
nautobot/core/celery/encoders.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
+
from django.db import models
|
|
3
4
|
from rest_framework.utils.encoders import JSONEncoder
|
|
4
5
|
|
|
5
6
|
logger = logging.getLogger(__name__)
|
|
@@ -26,10 +27,9 @@ class NautobotKombuJSONEncoder(JSONEncoder):
|
|
|
26
27
|
def default(self, obj):
|
|
27
28
|
# Import here to avoid django.core.exceptions.ImproperlyConfigured Error.
|
|
28
29
|
# Core App is not set up yet if we import this at the top of the file.
|
|
29
|
-
from nautobot.core.models import BaseModel
|
|
30
30
|
from nautobot.core.models.managers import TagsManager
|
|
31
31
|
|
|
32
|
-
if isinstance(obj,
|
|
32
|
+
if isinstance(obj, models.Model):
|
|
33
33
|
cls = obj.__class__
|
|
34
34
|
module = cls.__module__
|
|
35
35
|
qual_name = ".".join([module, cls.__qualname__]) # fully qualified dotted import path
|
nautobot/core/forms/fields.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import logging
|
|
2
3
|
import re
|
|
3
4
|
|
|
4
5
|
from django import forms as django_forms
|
|
@@ -11,6 +12,7 @@ from django.db.models import Q
|
|
|
11
12
|
from django.forms.fields import BoundField, CallableChoiceIterator, InvalidJSONInput, JSONField as _JSONField
|
|
12
13
|
from django.templatetags.static import static
|
|
13
14
|
from django.urls import reverse
|
|
15
|
+
from django.urls.exceptions import NoReverseMatch
|
|
14
16
|
from django.utils.html import format_html
|
|
15
17
|
import django_filters
|
|
16
18
|
from netaddr import EUI
|
|
@@ -21,6 +23,8 @@ from nautobot.core.forms import widgets
|
|
|
21
23
|
from nautobot.core.models import validators
|
|
22
24
|
from nautobot.core.utils import data as data_utils, lookup
|
|
23
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
24
28
|
__all__ = (
|
|
25
29
|
"CSVChoiceField",
|
|
26
30
|
"CSVContentTypeField",
|
|
@@ -591,8 +595,16 @@ class DynamicModelChoiceMixin:
|
|
|
591
595
|
widget = bound_field.field.widget
|
|
592
596
|
if not widget.attrs.get("data-url"):
|
|
593
597
|
route = lookup.get_route_for_model(self.queryset.model, "list", api=True)
|
|
594
|
-
|
|
595
|
-
|
|
598
|
+
try:
|
|
599
|
+
data_url = reverse(route)
|
|
600
|
+
widget.attrs["data-url"] = data_url
|
|
601
|
+
except NoReverseMatch:
|
|
602
|
+
logger.error(
|
|
603
|
+
'API route lookup "%s" failed for model %s, form field "%s" will not work properly',
|
|
604
|
+
route,
|
|
605
|
+
self.queryset.model.__name__,
|
|
606
|
+
bound_field.name,
|
|
607
|
+
)
|
|
596
608
|
|
|
597
609
|
return bound_field
|
|
598
610
|
|
|
@@ -800,9 +812,13 @@ class NumericArrayField(SimpleArrayField):
|
|
|
800
812
|
def to_python(self, value):
|
|
801
813
|
try:
|
|
802
814
|
if not value:
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
815
|
+
return []
|
|
816
|
+
|
|
817
|
+
if isinstance(value, list):
|
|
818
|
+
value = ",".join([str(n) for n in value])
|
|
819
|
+
|
|
820
|
+
value = ",".join([str(n) for n in forms.parse_numeric_range(value)])
|
|
821
|
+
|
|
806
822
|
except (TypeError, ValueError) as error:
|
|
807
823
|
raise ValidationError(error)
|
|
808
824
|
return super().to_python(value)
|
nautobot/core/forms/utils.py
CHANGED
|
@@ -41,6 +41,7 @@ def parse_numeric_range(input_string, base=10):
|
|
|
41
41
|
raise TypeError("Input value must be a string using a range format.")
|
|
42
42
|
except ValueError:
|
|
43
43
|
begin, end = dash_range, dash_range
|
|
44
|
+
|
|
44
45
|
begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
|
|
45
46
|
values.extend(range(begin, end))
|
|
46
47
|
# Remove duplicates and sort
|
|
@@ -108,7 +108,7 @@ class BulkEditObjects(Job):
|
|
|
108
108
|
if form.cleaned_data[field_name]:
|
|
109
109
|
getattr(obj, field_name).set(form.cleaned_data[field_name])
|
|
110
110
|
# Normal fields
|
|
111
|
-
elif form.cleaned_data[field_name] not in (None, ""):
|
|
111
|
+
elif form.cleaned_data[field_name] not in (None, "", []):
|
|
112
112
|
setattr(obj, field_name, form.cleaned_data[field_name])
|
|
113
113
|
|
|
114
114
|
# Update custom fields
|
|
@@ -213,6 +213,7 @@ class Command(BaseCommand):
|
|
|
213
213
|
_create_batch(PlatformFactory, 20, description="with Manufacturers", has_manufacturer=True)
|
|
214
214
|
_create_batch(PlatformFactory, 5, description="without Manufacturers", has_manufacturer=False)
|
|
215
215
|
_create_batch(SoftwareVersionFactory, 20, description="to be usable by Devices")
|
|
216
|
+
_create_batch(ExternalIntegrationFactory, 20)
|
|
216
217
|
_create_batch(SoftwareImageFileFactory, 25, description="to be usable by DeviceTypes")
|
|
217
218
|
_create_batch(ManufacturerFactory, 4, description="without Platforms") # 4 more hard-coded Manufacturers
|
|
218
219
|
_create_batch(DeviceTypeFactory, 30)
|
|
@@ -312,7 +313,6 @@ class Command(BaseCommand):
|
|
|
312
313
|
has_pp_info=True,
|
|
313
314
|
has_description=True,
|
|
314
315
|
)
|
|
315
|
-
_create_batch(ExternalIntegrationFactory, 20)
|
|
316
316
|
_create_batch(ControllerFactory, 10, description="with Devices or DeviceRedundancyGroups")
|
|
317
317
|
_create_batch(ControllerManagedDeviceGroupFactory, 5, description="without any Devices")
|
|
318
318
|
_create_batch(SupportedDataRateFactory, 20)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from django.contrib.contenttypes.models import ContentType
|
|
2
2
|
from django.db import models
|
|
3
|
+
from django.utils.html import format_html
|
|
3
4
|
|
|
4
5
|
from nautobot.core.choices import ColorChoices
|
|
5
6
|
from nautobot.core.constants import CHARFIELD_MAX_LENGTH
|
|
6
7
|
from nautobot.core.models import BaseManager, BaseModel, ContentTypeRelatedQuerySet
|
|
7
8
|
from nautobot.core.models.fields import ColorField
|
|
9
|
+
from nautobot.core.templatetags import helpers
|
|
8
10
|
from nautobot.extras.models.change_logging import ChangeLoggedModel
|
|
9
11
|
|
|
10
12
|
# Importing CustomFieldModel, ChangeLoggedModel, RelationshipModel from nautobot.extras.models
|
|
@@ -54,3 +56,10 @@ class NameColorContentTypesModel(
|
|
|
54
56
|
|
|
55
57
|
def get_content_types(self):
|
|
56
58
|
return ",".join(f"{ct.app_label}.{ct.model}" for ct in self.content_types.all())
|
|
59
|
+
|
|
60
|
+
def get_color_display(self):
|
|
61
|
+
if self.color:
|
|
62
|
+
return format_html(
|
|
63
|
+
'<span class="label color-block" style="background-color: #{}"> </span>', self.color
|
|
64
|
+
)
|
|
65
|
+
return helpers.placeholder(self.color)
|
|
@@ -27,8 +27,15 @@ class EnhancedURLValidator(URLValidator):
|
|
|
27
27
|
r"\Z",
|
|
28
28
|
re.IGNORECASE,
|
|
29
29
|
)
|
|
30
|
+
|
|
30
31
|
schemes = settings.ALLOWED_URL_SCHEMES
|
|
31
32
|
|
|
33
|
+
def __getattribute__(self, name):
|
|
34
|
+
"""Dynamically fetch schemes each time it's accessed."""
|
|
35
|
+
if name == "schemes":
|
|
36
|
+
self.schemes = settings.ALLOWED_URL_SCHEMES # Always return the latest list
|
|
37
|
+
return super().__getattribute__(name)
|
|
38
|
+
|
|
32
39
|
|
|
33
40
|
class ExclusionValidator(BaseValidator):
|
|
34
41
|
"""
|
nautobot/core/settings.py
CHANGED
|
@@ -287,20 +287,6 @@ TEST_USE_FACTORIES = is_truthy(os.getenv("NAUTOBOT_TEST_USE_FACTORIES", "False")
|
|
|
287
287
|
# Pseudo-random number generator seed, for reproducibility of test results.
|
|
288
288
|
TEST_FACTORY_SEED = os.getenv("NAUTOBOT_TEST_FACTORY_SEED", None)
|
|
289
289
|
|
|
290
|
-
#
|
|
291
|
-
# django-slowtests
|
|
292
|
-
#
|
|
293
|
-
|
|
294
|
-
# Performance test uses `NautobotPerformanceTestRunner` to run, which is only available once you have `django-slowtests` installed in your dev environment.
|
|
295
|
-
# `invoke performance-test` and adding `--performance-report` or `--performance-snapshot` at the end of the `invoke` command
|
|
296
|
-
# will automatically opt to NautobotPerformanceTestRunner to run the tests.
|
|
297
|
-
|
|
298
|
-
# The baseline file that the performance test is running against
|
|
299
|
-
# TODO we need to replace the baselines in this file with more consistent results at least for CI
|
|
300
|
-
TEST_PERFORMANCE_BASELINE_FILE = os.getenv(
|
|
301
|
-
"NAUTOBOT_TEST_PERFORMANCE_BASELINE_FILE", "nautobot/core/tests/performance_baselines.yml"
|
|
302
|
-
)
|
|
303
|
-
|
|
304
290
|
#
|
|
305
291
|
# Django Prometheus
|
|
306
292
|
#
|
nautobot/core/settings.yaml
CHANGED
|
@@ -1926,34 +1926,6 @@ properties:
|
|
|
1926
1926
|
version_added: "1.5.0"
|
|
1927
1927
|
see_also:
|
|
1928
1928
|
"`TEST_USE_FACTORIES`": "#test_use_factories"
|
|
1929
|
-
TEST_PERFORMANCE_BASELINE_FILE:
|
|
1930
|
-
default: "nautobot/core/tests/performance_baselines.yml"
|
|
1931
|
-
description: "File path of a YAML file providing baseline times for all performance-related tests."
|
|
1932
|
-
details: |-
|
|
1933
|
-
The YAML file should conform to the following format:
|
|
1934
|
-
|
|
1935
|
-
```yaml
|
|
1936
|
-
tests:
|
|
1937
|
-
- name: >-
|
|
1938
|
-
test_run_job_with_sensitive_variables_and_requires_approval
|
|
1939
|
-
(nautobot.extras.tests.test_views.JobTestCase)
|
|
1940
|
-
execution_time: 4.799533
|
|
1941
|
-
- name: test_run_missing_schedule (nautobot.extras.tests.test_views.JobTestCase)
|
|
1942
|
-
execution_time: 4.367563
|
|
1943
|
-
- name: test_run_now_missing_args (nautobot.extras.tests.test_views.JobTestCase)
|
|
1944
|
-
execution_time: 4.363194
|
|
1945
|
-
- name: >-
|
|
1946
|
-
test_create_object_with_constrained_permission
|
|
1947
|
-
(nautobot.extras.tests.test_views.GraphQLQueriesTestCase)
|
|
1948
|
-
execution_time: 3.474244
|
|
1949
|
-
- name: >-
|
|
1950
|
-
test_run_now_constrained_permissions
|
|
1951
|
-
(nautobot.extras.tests.test_views.JobTestCase)
|
|
1952
|
-
execution_time: 2.727531
|
|
1953
|
-
```
|
|
1954
|
-
environment_variable: "NAUTOBOT_TEST_PERFORMANCE_BASELINE_FILE"
|
|
1955
|
-
type: "string"
|
|
1956
|
-
version_added: "1.5.0"
|
|
1957
1929
|
TEST_USE_FACTORIES:
|
|
1958
1930
|
default: false
|
|
1959
1931
|
description: >-
|
nautobot/core/tables.py
CHANGED
|
@@ -493,6 +493,9 @@ class LinkedCountColumn(django_tables2.Column):
|
|
|
493
493
|
reverse_lookup (str, optional): The reverse lookup parameter to use to derive the count.
|
|
494
494
|
If not specified, the first key in `url_params` will be implicitly used as the `reverse_lookup` value.
|
|
495
495
|
distinct (bool, optional): Parameter passed through to `count_related()`.
|
|
496
|
+
display_field (str, optional): Name of the field to use when displaying an object rather than just a count.
|
|
497
|
+
This will be passed to hyperlinked_object() as the `field` parameter
|
|
498
|
+
If not specified, it will use the "display" field.
|
|
496
499
|
**kwargs (dict, optional): As the parent Column class.
|
|
497
500
|
|
|
498
501
|
Examples:
|
|
@@ -536,6 +539,7 @@ class LinkedCountColumn(django_tables2.Column):
|
|
|
536
539
|
reverse_lookup=None,
|
|
537
540
|
distinct=False,
|
|
538
541
|
default=None,
|
|
542
|
+
display_field="display",
|
|
539
543
|
**kwargs,
|
|
540
544
|
):
|
|
541
545
|
self.viewname = viewname
|
|
@@ -544,6 +548,7 @@ class LinkedCountColumn(django_tables2.Column):
|
|
|
544
548
|
self.url_params = url_params
|
|
545
549
|
self.reverse_lookup = reverse_lookup or next(iter(url_params.keys()))
|
|
546
550
|
self.distinct = distinct
|
|
551
|
+
self.display_field = display_field
|
|
547
552
|
self.model = get_model_for_view_name(self.viewname)
|
|
548
553
|
super().__init__(*args, default=default, **kwargs)
|
|
549
554
|
|
|
@@ -565,7 +570,7 @@ class LinkedCountColumn(django_tables2.Column):
|
|
|
565
570
|
if value > 1:
|
|
566
571
|
return format_html('<a href="{}" class="badge">{}</a>', url, value)
|
|
567
572
|
if related_record is not None:
|
|
568
|
-
return helpers.hyperlinked_object(related_record)
|
|
573
|
+
return helpers.hyperlinked_object(related_record, self.display_field)
|
|
569
574
|
if value == 1:
|
|
570
575
|
return format_html('<a href="{}" class="badge">{}</a>', url, value)
|
|
571
576
|
return helpers.placeholder(value)
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
{% if perms.extras.view_note %}
|
|
118
118
|
{% if active_tab != 'notes' and object.get_notes_url or active_tab == 'notes' %}
|
|
119
119
|
<li role="presentation"{% if active_tab == 'notes' %} class="active"{% endif %}>
|
|
120
|
-
<a href="{{ object.get_notes_url }}">Notes</a>
|
|
120
|
+
<a href="{{ object.get_notes_url }}">Notes {% badge object.notes.count %}</a>
|
|
121
121
|
</li>
|
|
122
122
|
{% endif %}
|
|
123
123
|
{% endif %}
|
nautobot/core/testing/api.py
CHANGED
|
@@ -702,6 +702,9 @@ class APIViewTestCases:
|
|
|
702
702
|
else:
|
|
703
703
|
self.assertEqual(obj.key, expected_slug)
|
|
704
704
|
|
|
705
|
+
# TODO: The override_settings here is a temporary workaround for not breaking any app tests
|
|
706
|
+
# long term fix should be using appropriate object permissions instead of the blanket override
|
|
707
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
705
708
|
def test_create_object(self):
|
|
706
709
|
"""
|
|
707
710
|
POST a single object with permission.
|
|
@@ -738,6 +741,9 @@ class APIViewTestCases:
|
|
|
738
741
|
self.assertEqual(len(objectchanges), 1)
|
|
739
742
|
self.assertEqual(objectchanges[0].action, extras_choices.ObjectChangeActionChoices.ACTION_CREATE)
|
|
740
743
|
|
|
744
|
+
# TODO: The override_settings here is a temporary workaround for not breaking any app tests
|
|
745
|
+
# long term fix should be using appropriate object permissions instead of the blanket override
|
|
746
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
741
747
|
def test_recreate_object_csv(self):
|
|
742
748
|
"""CSV export an object, delete it, and recreate it via CSV import."""
|
|
743
749
|
if hasattr(self, "get_deletable_object"):
|
|
@@ -801,6 +807,9 @@ class APIViewTestCases:
|
|
|
801
807
|
f"{field_name} should have been unchanged on delete/recreate but it differs!",
|
|
802
808
|
)
|
|
803
809
|
|
|
810
|
+
# TODO: The override_settings here is a temporary workaround for not breaking any app tests
|
|
811
|
+
# long term fix should be using appropriate object permissions instead of the blanket override
|
|
812
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
804
813
|
def test_bulk_create_objects(self):
|
|
805
814
|
"""
|
|
806
815
|
POST a set of objects in a single request.
|
|
@@ -853,6 +862,9 @@ class APIViewTestCases:
|
|
|
853
862
|
response = self.client.patch(url, update_data, format="json", **self.header)
|
|
854
863
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
855
864
|
|
|
865
|
+
# TODO: The override_settings here is a temporary workaround for not breaking any app tests
|
|
866
|
+
# long term fix should be using appropriate object permissions instead of the blanket override
|
|
867
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
856
868
|
def test_update_object(self):
|
|
857
869
|
"""
|
|
858
870
|
PATCH a single object identified by its ID.
|
|
@@ -965,6 +977,9 @@ class APIViewTestCases:
|
|
|
965
977
|
instance.refresh_from_db()
|
|
966
978
|
self.assertInstanceEqual(instance, update_data, exclude=self.validation_excluded_fields, api=True)
|
|
967
979
|
|
|
980
|
+
# TODO: The override_settings here is a temporary workaround for not breaking any app tests
|
|
981
|
+
# long term fix should be using appropriate object permissions instead of the blanket override
|
|
982
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
968
983
|
def test_get_put_round_trip(self):
|
|
969
984
|
"""GET and then PUT an object and verify that it's accepted and unchanged."""
|
|
970
985
|
self.maxDiff = None
|
|
@@ -995,6 +1010,9 @@ class APIViewTestCases:
|
|
|
995
1010
|
updated_serialized_object.pop("last_updated", None)
|
|
996
1011
|
self.assertEqual(initial_serialized_object, updated_serialized_object)
|
|
997
1012
|
|
|
1013
|
+
# TODO: The override_settings here is a temporary workaround for not breaking any app tests
|
|
1014
|
+
# long term fix should be using appropriate object permissions instead of the blanket override
|
|
1015
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
998
1016
|
def test_bulk_update_objects(self):
|
|
999
1017
|
"""
|
|
1000
1018
|
PATCH a set of objects in a single request.
|
|
@@ -44,8 +44,6 @@ CONSTANCE_BACKEND = "constance.backends.memory.MemoryBackend"
|
|
|
44
44
|
TEST_USE_FACTORIES = True
|
|
45
45
|
# For now, use a constant PRNG seed for consistent results. In the future we can remove this for fuzzier testing.
|
|
46
46
|
TEST_FACTORY_SEED = "Nautobot"
|
|
47
|
-
# File in which all performance-specifc test baselines are stored
|
|
48
|
-
TEST_PERFORMANCE_BASELINE_FILE = "nautobot/core/tests/performance_baselines.yml"
|
|
49
47
|
|
|
50
48
|
# Make Celery run synchronously (eager), to always store eager results, and run the broker in-memory.
|
|
51
49
|
# NOTE: Celery does not honor the TASK_TRACK_STARTED config when running in eager mode, so the job result is not saved until after the task completes.
|
nautobot/core/tests/runner.py
CHANGED
|
@@ -14,7 +14,6 @@ from django.db import connections
|
|
|
14
14
|
from django.db.migrations.recorder import MigrationRecorder
|
|
15
15
|
from django.test.runner import _init_worker, DiscoverRunner, ParallelTestSuite
|
|
16
16
|
from django.test.utils import get_unique_databases_and_mirrors, NullTimeKeeper, override_settings
|
|
17
|
-
import yaml
|
|
18
17
|
|
|
19
18
|
from nautobot.core.celery import app, setup_nautobot_job_logging
|
|
20
19
|
from nautobot.core.settings_funcs import parse_redis_connection
|
|
@@ -40,21 +39,25 @@ class NautobotParallelTestSuite(ParallelTestSuite):
|
|
|
40
39
|
|
|
41
40
|
class NautobotTestRunner(DiscoverRunner):
|
|
42
41
|
"""
|
|
43
|
-
Custom test runner that excludes integration tests by default.
|
|
42
|
+
Custom test runner that excludes (slow) integration and migration tests by default.
|
|
44
43
|
|
|
45
44
|
This test runner is aware of our use of the "integration" tag and only runs integration tests if
|
|
46
45
|
explicitly passed in with `nautobot-server test --tag integration`.
|
|
46
|
+
Similarly, it only runs migration tests if explicitly called with `--tag migration_test`.
|
|
47
47
|
|
|
48
48
|
By Nautobot convention, integration tests must be tagged with "integration". The base
|
|
49
49
|
`nautobot.core.testing.integration.SeleniumTestCase` has this tag, therefore any test cases
|
|
50
50
|
inheriting from that class do not need to be explicitly tagged.
|
|
51
51
|
|
|
52
52
|
Only integration tests that DO NOT inherit from `SeleniumTestCase` will need to be explicitly tagged.
|
|
53
|
+
|
|
54
|
+
Similarly, the `django-test-migrations` package `MigratorTestCase` base class has the tag `migration_test`, so
|
|
55
|
+
any subclasses thereof do not need to be explicitly tagged.
|
|
53
56
|
"""
|
|
54
57
|
|
|
55
58
|
parallel_test_suite = NautobotParallelTestSuite
|
|
56
59
|
|
|
57
|
-
exclude_tags = ["integration"]
|
|
60
|
+
exclude_tags = ["integration", "migration_test"]
|
|
58
61
|
|
|
59
62
|
@classmethod
|
|
60
63
|
def add_arguments(cls, parser):
|
|
@@ -75,15 +78,19 @@ class NautobotTestRunner(DiscoverRunner):
|
|
|
75
78
|
self.cache_test_fixtures = cache_test_fixtures
|
|
76
79
|
self.reusedb = reusedb
|
|
77
80
|
|
|
78
|
-
# Assert "integration" hasn't been provided w/ --tag
|
|
79
81
|
incoming_tags = kwargs.get("tags") or []
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
exclude_tags = kwargs.get("exclude_tags") or []
|
|
83
|
+
|
|
84
|
+
for default_excluded_tag in self.exclude_tags:
|
|
85
|
+
if default_excluded_tag not in incoming_tags:
|
|
86
|
+
exclude_tags.append(default_excluded_tag)
|
|
87
|
+
# Can't just use self.log() here because we haven't yet called super().__init__()
|
|
88
|
+
if logger := kwargs.get("logger"):
|
|
89
|
+
logger.info("Implicitly excluding tests tagged %r", default_excluded_tag)
|
|
90
|
+
elif kwargs.get("verbosity", 1) >= 1:
|
|
91
|
+
print(f"Implicitly excluding tests tagged {default_excluded_tag!r}")
|
|
82
92
|
|
|
83
|
-
|
|
84
|
-
if "integration" not in incoming_tags:
|
|
85
|
-
incoming_exclude_tags.extend(self.exclude_tags)
|
|
86
|
-
kwargs["exclude_tags"] = incoming_exclude_tags
|
|
93
|
+
kwargs["exclude_tags"] = exclude_tags
|
|
87
94
|
|
|
88
95
|
super().__init__(**kwargs)
|
|
89
96
|
|
|
@@ -202,133 +209,3 @@ class NautobotTestRunner(DiscoverRunner):
|
|
|
202
209
|
print(f"Database {db_name} emptied!")
|
|
203
210
|
|
|
204
211
|
connection.creation.destroy_test_db(old_name, self.verbosity, self.keepdb)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
# Use django_slowtests only when GENERATE_PERFORMANCE_REPORT flag is set to true
|
|
208
|
-
try:
|
|
209
|
-
from django_slowtests.testrunner import DiscoverSlowestTestsRunner
|
|
210
|
-
|
|
211
|
-
print("Using NautobotPerformanceTestRunner to run tests ...")
|
|
212
|
-
|
|
213
|
-
class NautobotPerformanceTestRunner(NautobotTestRunner, DiscoverSlowestTestsRunner):
|
|
214
|
-
"""
|
|
215
|
-
Pre-requisite:
|
|
216
|
-
Set `GENERATE_PERFORMANCE_REPORT` to True in settings.py
|
|
217
|
-
This test runner is designated to run performance specific unit tests.
|
|
218
|
-
|
|
219
|
-
`ModelViewTestCase` is tagged with `performance` to test the time it will take to retrieve, list, create, bulk_create,
|
|
220
|
-
delete, bulk_delete, edit, bulk_edit object(s) and various other operations.
|
|
221
|
-
|
|
222
|
-
The results are compared to the corresponding entries in `TEST_PERFORMANCE_BASELINE_FILE` and only results that are significantly slower
|
|
223
|
-
than baseline will be exposed to the user.
|
|
224
|
-
"""
|
|
225
|
-
|
|
226
|
-
def generate_report(self, test_results, result):
|
|
227
|
-
"""
|
|
228
|
-
Generate Performance Report consists of unit tests that are significantly slower than baseline.
|
|
229
|
-
"""
|
|
230
|
-
test_result_count = len(test_results)
|
|
231
|
-
|
|
232
|
-
# Add `--performance-snapshot` to the end of `invoke` commands to generate a report.json file consist of the performance tests result
|
|
233
|
-
if self.report_path:
|
|
234
|
-
data = [
|
|
235
|
-
{
|
|
236
|
-
"tests": [
|
|
237
|
-
{
|
|
238
|
-
"name": func_name,
|
|
239
|
-
"execution_time": float(timing),
|
|
240
|
-
}
|
|
241
|
-
for func_name, timing in test_results
|
|
242
|
-
],
|
|
243
|
-
"test_count": result.testsRun,
|
|
244
|
-
"failed_count": len(result.errors + result.failures),
|
|
245
|
-
"total_execution_time": result.timeTaken,
|
|
246
|
-
}
|
|
247
|
-
]
|
|
248
|
-
with open(self.report_path, "w") as outfile:
|
|
249
|
-
yaml.dump(data, outfile, sort_keys=False)
|
|
250
|
-
# Print the results in the CLI.
|
|
251
|
-
else:
|
|
252
|
-
if test_result_count:
|
|
253
|
-
print(f"\n{test_result_count} abnormally slower tests:")
|
|
254
|
-
for func_name, timing in test_results:
|
|
255
|
-
time = float(timing)
|
|
256
|
-
baseline = self.baselines.get(func_name, None)
|
|
257
|
-
if baseline:
|
|
258
|
-
baseline = float(baseline)
|
|
259
|
-
print(f"{time:.4f}s {func_name} is significantly slower than the baseline {baseline:.4f}s")
|
|
260
|
-
else:
|
|
261
|
-
print(
|
|
262
|
-
f"Performance baseline for {func_name} is not available. Test took {time:.4f}s to run"
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
if not test_results:
|
|
266
|
-
print("\nNo tests signficantly slower than baseline. Success!")
|
|
267
|
-
|
|
268
|
-
def get_baselines(self):
|
|
269
|
-
"""Load the performance_baselines.yml file for result comparison."""
|
|
270
|
-
baselines = {}
|
|
271
|
-
input_file = getattr(
|
|
272
|
-
settings, "TEST_PERFORMANCE_BASELINE_FILE", "nautobot/core/tests/performance_baselines.yml"
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
with open(input_file) as f:
|
|
276
|
-
data = yaml.safe_load(f)
|
|
277
|
-
for entry in data["tests"]:
|
|
278
|
-
baselines[entry["name"]] = entry["execution_time"]
|
|
279
|
-
return baselines
|
|
280
|
-
|
|
281
|
-
def suite_result(self, suite, result):
|
|
282
|
-
"""Compile the performance test results"""
|
|
283
|
-
return_value = super(DiscoverSlowestTestsRunner, self).suite_result(suite, result)
|
|
284
|
-
self.baselines = self.get_baselines()
|
|
285
|
-
|
|
286
|
-
# add `--performance_report` to `invoke` commands to generate report.
|
|
287
|
-
# e.g. `invoke unittest --performance_report`
|
|
288
|
-
if not self.should_generate_report:
|
|
289
|
-
self.remove_timing_tmp_files()
|
|
290
|
-
return return_value
|
|
291
|
-
|
|
292
|
-
# Grab slowest tests
|
|
293
|
-
timings = self.get_timings()
|
|
294
|
-
# Sort the results by test names x[0]
|
|
295
|
-
by_name = sorted(timings, key=lambda x: x[0])
|
|
296
|
-
test_results = by_name
|
|
297
|
-
|
|
298
|
-
if self.baselines:
|
|
299
|
-
# Filter tests by baseline numbers
|
|
300
|
-
test_results = []
|
|
301
|
-
|
|
302
|
-
for entry in by_name:
|
|
303
|
-
# Convert test time from seconds to miliseconds for comparison
|
|
304
|
-
result_time_ms = entry[1] * 1000
|
|
305
|
-
# If self.report_path, that means the user wants to update the performance baselines.
|
|
306
|
-
# so we append every result that is available to us.
|
|
307
|
-
if self.report_path:
|
|
308
|
-
test_results.append(entry)
|
|
309
|
-
else:
|
|
310
|
-
# If the test is completed under 1.5 times the baseline or the difference between the result and the baseline is less than 3 seconds,
|
|
311
|
-
# dont show the test to the user.
|
|
312
|
-
|
|
313
|
-
baseline = self.baselines.get(entry[0], None)
|
|
314
|
-
|
|
315
|
-
# check if baseline is available
|
|
316
|
-
if not baseline:
|
|
317
|
-
test_results.append(entry)
|
|
318
|
-
continue
|
|
319
|
-
|
|
320
|
-
# baseline duration in milliseconds
|
|
321
|
-
baseline_ms = baseline * 1000
|
|
322
|
-
# Arbitrary criteria to not make performance test fail easily
|
|
323
|
-
if result_time_ms <= baseline_ms * 1.5 or result_time_ms - baseline_ms <= 500:
|
|
324
|
-
continue
|
|
325
|
-
|
|
326
|
-
test_results.append(entry)
|
|
327
|
-
|
|
328
|
-
self.generate_report(test_results, result)
|
|
329
|
-
return return_value
|
|
330
|
-
|
|
331
|
-
except ImportError:
|
|
332
|
-
print(
|
|
333
|
-
"Unable to import DiscoverSlowestTestsRunner from `django_slowtests`. Is the 'django_slowtests' package installed?"
|
|
334
|
-
)
|
nautobot/core/tests/test_api.py
CHANGED
|
@@ -639,7 +639,7 @@ class WritableNestedSerializerTest(testing.APITestCase):
|
|
|
639
639
|
"vlan_group": self.vlan_group1.pk,
|
|
640
640
|
}
|
|
641
641
|
url = reverse("ipam-api:vlan-list")
|
|
642
|
-
self.add_permissions("ipam.add_vlan")
|
|
642
|
+
self.add_permissions("ipam.add_vlan", "ipam.view_vlangroup", "extras.view_status")
|
|
643
643
|
|
|
644
644
|
response = self.client.post(url, data, format="json", **self.header)
|
|
645
645
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
@@ -672,7 +672,7 @@ class WritableNestedSerializerTest(testing.APITestCase):
|
|
|
672
672
|
"vlan_group": {"name": self.vlan_group1.name},
|
|
673
673
|
}
|
|
674
674
|
url = reverse("ipam-api:vlan-list")
|
|
675
|
-
self.add_permissions("ipam.add_vlan")
|
|
675
|
+
self.add_permissions("ipam.add_vlan", "ipam.view_vlangroup", "extras.view_status")
|
|
676
676
|
|
|
677
677
|
response = self.client.post(url, data, format="json", **self.header)
|
|
678
678
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
@@ -708,7 +708,7 @@ class WritableNestedSerializerTest(testing.APITestCase):
|
|
|
708
708
|
},
|
|
709
709
|
}
|
|
710
710
|
url = reverse("ipam-api:vlan-list")
|
|
711
|
-
self.add_permissions("ipam.add_vlan")
|
|
711
|
+
self.add_permissions("ipam.add_vlan", "ipam.view_vlangroup", "extras.view_status")
|
|
712
712
|
|
|
713
713
|
with testing.disable_warnings("django.request"):
|
|
714
714
|
response = self.client.post(url, data, format="json", **self.header)
|
|
@@ -775,7 +775,7 @@ class WritableNestedSerializerTest(testing.APITestCase):
|
|
|
775
775
|
"vlan_group": self.vlan_group1.pk,
|
|
776
776
|
}
|
|
777
777
|
url = reverse("ipam-api:vlan-list")
|
|
778
|
-
self.add_permissions("ipam.add_vlan")
|
|
778
|
+
self.add_permissions("ipam.add_vlan", "ipam.view_vlangroup", "extras.view_status")
|
|
779
779
|
|
|
780
780
|
response = self.client.post(url, data, format="json", **self.header)
|
|
781
781
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|