nautobot 2.3.15__py3-none-any.whl → 2.3.16__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/circuits/views.py +3 -3
- nautobot/cloud/models.py +1 -1
- nautobot/core/api/fields.py +5 -5
- nautobot/core/api/serializers.py +9 -9
- nautobot/core/api/views.py +3 -2
- nautobot/core/apps/__init__.py +5 -2
- nautobot/core/celery/schedulers.py +1 -1
- nautobot/core/filters.py +19 -16
- nautobot/core/forms/fields.py +5 -5
- nautobot/core/graphql/types.py +1 -1
- nautobot/core/jobs/__init__.py +4 -4
- nautobot/core/jobs/cleanup.py +1 -1
- nautobot/core/jobs/groups.py +1 -1
- nautobot/core/management/commands/validate_models.py +1 -1
- nautobot/core/models/__init__.py +1 -1
- nautobot/core/models/query_functions.py +2 -2
- nautobot/core/models/tree_queries.py +2 -2
- nautobot/core/tables.py +5 -5
- nautobot/core/testing/filters.py +7 -3
- nautobot/core/testing/views.py +5 -0
- nautobot/core/tests/runner.py +1 -1
- nautobot/core/views/generic.py +51 -43
- nautobot/core/views/mixins.py +21 -11
- nautobot/dcim/api/serializers.py +48 -48
- nautobot/dcim/forms.py +2 -0
- nautobot/dcim/graphql/types.py +2 -2
- nautobot/dcim/models/device_component_templates.py +2 -2
- nautobot/dcim/models/device_components.py +22 -20
- nautobot/dcim/models/devices.py +1 -1
- nautobot/dcim/models/locations.py +3 -3
- nautobot/dcim/models/power.py +6 -5
- nautobot/dcim/models/racks.py +4 -4
- nautobot/dcim/tables/__init__.py +3 -3
- nautobot/dcim/tables/devicetypes.py +2 -2
- nautobot/dcim/tests/test_filters.py +1 -0
- nautobot/dcim/tests/test_graphql.py +52 -0
- nautobot/dcim/tests/test_models.py +4 -1
- nautobot/dcim/views.py +1 -1
- nautobot/extras/api/customfields.py +2 -2
- nautobot/extras/api/serializers.py +72 -69
- nautobot/extras/api/views.py +4 -4
- nautobot/extras/health_checks.py +1 -2
- nautobot/extras/jobs.py +5 -5
- nautobot/extras/managers.py +3 -1
- nautobot/extras/migrations/0018_joblog_data_migration.py +7 -9
- nautobot/extras/models/groups.py +13 -9
- nautobot/extras/models/jobs.py +4 -4
- nautobot/extras/models/models.py +2 -2
- nautobot/extras/plugins/views.py +1 -1
- nautobot/extras/tables.py +5 -5
- nautobot/extras/test_jobs/api_test_job.py +1 -1
- nautobot/extras/test_jobs/atomic_transaction.py +2 -2
- nautobot/extras/test_jobs/dry_run.py +1 -1
- nautobot/extras/test_jobs/fail.py +5 -5
- nautobot/extras/test_jobs/file_output.py +1 -1
- nautobot/extras/test_jobs/file_upload_fail.py +1 -1
- nautobot/extras/test_jobs/file_upload_pass.py +1 -1
- nautobot/extras/test_jobs/ipaddress_vars.py +3 -1
- nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +1 -1
- nautobot/extras/test_jobs/location_with_custom_field.py +1 -1
- nautobot/extras/test_jobs/log_redaction.py +1 -1
- nautobot/extras/test_jobs/log_skip_db_logging.py +1 -1
- nautobot/extras/test_jobs/modify_db.py +1 -1
- nautobot/extras/test_jobs/object_var_optional.py +1 -1
- nautobot/extras/test_jobs/object_var_required.py +1 -1
- nautobot/extras/test_jobs/object_vars.py +1 -1
- nautobot/extras/test_jobs/pass.py +3 -3
- nautobot/extras/test_jobs/profiling.py +1 -1
- nautobot/extras/test_jobs/relative_import.py +3 -3
- nautobot/extras/test_jobs/soft_time_limit_greater_than_time_limit.py +1 -1
- nautobot/extras/test_jobs/task_queues.py +1 -1
- nautobot/extras/tests/test_api.py +13 -13
- nautobot/extras/tests/test_customfields.py +1 -1
- nautobot/extras/tests/test_datasources.py +2 -1
- nautobot/extras/tests/test_dynamicgroups.py +1 -1
- nautobot/extras/tests/test_filters.py +6 -6
- nautobot/extras/tests/test_jobs.py +11 -11
- nautobot/extras/tests/test_models.py +10 -10
- nautobot/extras/tests/test_relationships.py +1 -1
- nautobot/extras/tests/test_views.py +16 -16
- nautobot/extras/views.py +20 -16
- nautobot/ipam/api/fields.py +3 -3
- nautobot/ipam/api/serializers.py +33 -33
- nautobot/ipam/api/views.py +37 -61
- nautobot/ipam/querysets.py +2 -2
- nautobot/ipam/tests/test_api.py +12 -1
- nautobot/ipam/tests/test_forms.py +51 -47
- nautobot/ipam/tests/test_migrations.py +30 -30
- nautobot/ipam/tests/test_querysets.py +14 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.3.html +181 -99
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +270 -270
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/users/admin.py +1 -1
- nautobot/users/api/serializers.py +4 -4
- nautobot/users/api/views.py +1 -1
- nautobot/virtualization/api/serializers.py +4 -4
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/METADATA +1 -1
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/RECORD +106 -106
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/WHEEL +1 -1
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/NOTICE +0 -0
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/entry_points.txt +0 -0
nautobot/circuits/views.py
CHANGED
|
@@ -79,11 +79,11 @@ class CircuitTerminationUIViewSet(
|
|
|
79
79
|
obj.circuit = get_object_or_404(Circuit, pk=self.kwargs["circuit"])
|
|
80
80
|
return obj
|
|
81
81
|
|
|
82
|
-
def get_return_url(self, request, obj=None):
|
|
82
|
+
def get_return_url(self, request, obj=None, default_return_url=None):
|
|
83
83
|
if obj is not None and obj.present_in_database and obj.pk:
|
|
84
|
-
return super().get_return_url(request, obj=obj.circuit)
|
|
84
|
+
return super().get_return_url(request, obj=obj.circuit, default_return_url=default_return_url)
|
|
85
85
|
|
|
86
|
-
return super().get_return_url(request, obj=obj)
|
|
86
|
+
return super().get_return_url(request, obj=obj, default_return_url=default_return_url)
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
class ProviderUIViewSet(NautobotUIViewSet):
|
nautobot/cloud/models.py
CHANGED
|
@@ -109,7 +109,7 @@ class CloudResourceTypeMixin(models.Model):
|
|
|
109
109
|
super().clean()
|
|
110
110
|
|
|
111
111
|
# Copied from nautobot.extras.models.models.ConfigContextSchemaValidationMixin
|
|
112
|
-
schema = self.cloud_resource_type.config_schema
|
|
112
|
+
schema = self.cloud_resource_type.config_schema # pylint: disable=no-member
|
|
113
113
|
if schema:
|
|
114
114
|
try:
|
|
115
115
|
Draft7Validator(schema, format_checker=Draft7Validator.FORMAT_CHECKER).validate(self.extra_config)
|
nautobot/core/api/fields.py
CHANGED
|
@@ -53,10 +53,10 @@ class ChoiceField(serializers.Field):
|
|
|
53
53
|
data = ""
|
|
54
54
|
return super().validate_empty_values(data)
|
|
55
55
|
|
|
56
|
-
def to_representation(self,
|
|
57
|
-
if
|
|
56
|
+
def to_representation(self, value):
|
|
57
|
+
if value == "":
|
|
58
58
|
return None
|
|
59
|
-
return OrderedDict([("value",
|
|
59
|
+
return OrderedDict([("value", value), ("label", self._choices[value])])
|
|
60
60
|
|
|
61
61
|
def to_internal_value(self, data):
|
|
62
62
|
if data == "":
|
|
@@ -123,8 +123,8 @@ class ContentTypeField(RelatedField):
|
|
|
123
123
|
self.fail("invalid")
|
|
124
124
|
return None
|
|
125
125
|
|
|
126
|
-
def to_representation(self,
|
|
127
|
-
return f"{
|
|
126
|
+
def to_representation(self, value):
|
|
127
|
+
return f"{value.app_label}.{value.model}"
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
class LaxURLField(URLField):
|
nautobot/core/api/serializers.py
CHANGED
|
@@ -759,27 +759,27 @@ class ValidatedModelSerializer(BaseModelSerializer):
|
|
|
759
759
|
validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
|
|
760
760
|
"""
|
|
761
761
|
|
|
762
|
-
def validate(self,
|
|
762
|
+
def validate(self, attrs):
|
|
763
763
|
# Remove custom fields data and tags (if any) prior to model validation
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
764
|
+
local_attrs = attrs.copy()
|
|
765
|
+
local_attrs.pop("custom_fields", None)
|
|
766
|
+
local_attrs.pop("relationships", None)
|
|
767
|
+
local_attrs.pop("tags", None)
|
|
768
768
|
|
|
769
769
|
# Skip ManyToManyFields
|
|
770
770
|
for field in self.Meta.model._meta.get_fields():
|
|
771
771
|
if isinstance(field, models.ManyToManyField):
|
|
772
|
-
|
|
772
|
+
local_attrs.pop(field.name, None)
|
|
773
773
|
|
|
774
774
|
# Run clean() on an instance of the model
|
|
775
775
|
if self.instance is None:
|
|
776
|
-
instance = self.Meta.model(**
|
|
776
|
+
instance = self.Meta.model(**local_attrs)
|
|
777
777
|
else:
|
|
778
778
|
instance = self.instance
|
|
779
|
-
for k, v in
|
|
779
|
+
for k, v in local_attrs.items():
|
|
780
780
|
setattr(instance, k, v)
|
|
781
781
|
instance.full_clean()
|
|
782
|
-
return
|
|
782
|
+
return attrs
|
|
783
783
|
|
|
784
784
|
|
|
785
785
|
class WritableNestedSerializer(BaseModelSerializer):
|
nautobot/core/api/views.py
CHANGED
|
@@ -373,7 +373,7 @@ class APIRootView(AuthenticatedAPIRootView):
|
|
|
373
373
|
name = "API Root"
|
|
374
374
|
|
|
375
375
|
@extend_schema(exclude=True)
|
|
376
|
-
def get(self, request, format=None): # pylint: disable=redefined-builtin
|
|
376
|
+
def get(self, request, *args, format=None, **kwargs): # pylint: disable=redefined-builtin
|
|
377
377
|
return Response(
|
|
378
378
|
OrderedDict(
|
|
379
379
|
(
|
|
@@ -572,12 +572,13 @@ class GraphQLDRFAPIView(NautobotAPIVersionMixin, APIView):
|
|
|
572
572
|
middleware = None
|
|
573
573
|
root_value = None
|
|
574
574
|
|
|
575
|
-
def __init__(self, schema=None, executor=None, middleware=None, root_value=None, backend=None):
|
|
575
|
+
def __init__(self, schema=None, executor=None, middleware=None, root_value=None, backend=None, **kwargs):
|
|
576
576
|
self.schema = schema
|
|
577
577
|
self.executor = executor
|
|
578
578
|
self.middleware = middleware
|
|
579
579
|
self.root_value = root_value
|
|
580
580
|
self.backend = backend
|
|
581
|
+
super().__init__(**kwargs)
|
|
581
582
|
|
|
582
583
|
def get_root_value(self, request):
|
|
583
584
|
return self.root_value
|
nautobot/core/apps/__init__.py
CHANGED
|
@@ -592,6 +592,7 @@ class NavMenuGroup(NavMenuBase, PermissionsMixin):
|
|
|
592
592
|
elif not all(isinstance(item, NavMenuItem) for item in items):
|
|
593
593
|
raise TypeError("All items defined in a group must be an instance of NavMenuItem")
|
|
594
594
|
self.items = items
|
|
595
|
+
super().__init__(permissions=self.permissions)
|
|
595
596
|
|
|
596
597
|
|
|
597
598
|
class NavMenuItem(NavMenuBase, PermissionsMixin):
|
|
@@ -788,7 +789,7 @@ class NavGrouping(NavMenuBase, PermissionsMixin):
|
|
|
788
789
|
Items are each specified as a list of NavItem or NavGrouping instances.
|
|
789
790
|
"""
|
|
790
791
|
|
|
791
|
-
def __init__(self, name, items, weight=1000):
|
|
792
|
+
def __init__(self, name, items, weight=1000): # pylint:disable=super-init-not-called # this is dead code anyway
|
|
792
793
|
self.name = name
|
|
793
794
|
self.items = items
|
|
794
795
|
self.weight = weight
|
|
@@ -824,7 +825,9 @@ class NavItem(NavMenuBase, PermissionsMixin):
|
|
|
824
825
|
Links are specified as Django reverse URL strings.
|
|
825
826
|
"""
|
|
826
827
|
|
|
827
|
-
def __init__(
|
|
828
|
+
def __init__( # pylint:disable=super-init-not-called # this is dead code anyway
|
|
829
|
+
self, name, link, *args, permissions=None, weight=1000, **kwargs
|
|
830
|
+
):
|
|
828
831
|
self.name = name
|
|
829
832
|
self.link = link
|
|
830
833
|
self.permissions = permissions or []
|
|
@@ -19,7 +19,7 @@ class NautobotScheduleEntry(ModelEntry):
|
|
|
19
19
|
nautobot.extras.models.ScheduledJob model
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
def __init__(self, model, app=None):
|
|
22
|
+
def __init__(self, model, app=None): # pylint:disable=super-init-not-called # we must copy-and-paste from super
|
|
23
23
|
"""Initialize the model entry."""
|
|
24
24
|
# copy-paste from django_celery_beat.schedulers
|
|
25
25
|
self.app = app or current_app._get_current_object()
|
nautobot/core/filters.py
CHANGED
|
@@ -668,7 +668,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
668
668
|
the form `<field_name>__<lookup_expr>`
|
|
669
669
|
"""
|
|
670
670
|
magic_filters = {}
|
|
671
|
-
if filter_field.method is not None or filter_field.lookup_expr not in ["exact", "in"]:
|
|
671
|
+
if filter_field.method is not None or filter_field.lookup_expr not in ["exact", "in", "iexact"]:
|
|
672
672
|
return magic_filters
|
|
673
673
|
|
|
674
674
|
# Choose the lookup expression map based on the filter type
|
|
@@ -679,7 +679,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
679
679
|
|
|
680
680
|
# Get properties of the existing filter for later use
|
|
681
681
|
field_name = filter_field.field_name
|
|
682
|
-
field = get_model_field(cls._meta.model, field_name)
|
|
682
|
+
field = get_model_field(cls._meta.model, field_name) # pylint: disable=no-member
|
|
683
683
|
|
|
684
684
|
# If there isn't a model field, return.
|
|
685
685
|
if field is None:
|
|
@@ -696,7 +696,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
696
696
|
new_filter_name = f"{filter_name}__{lookup_name}"
|
|
697
697
|
|
|
698
698
|
try:
|
|
699
|
-
if filter_name in cls.declared_filters and lookup_expr not in {"isnull"}:
|
|
699
|
+
if filter_name in cls.declared_filters and lookup_expr not in {"isnull"}: # pylint: disable=no-member
|
|
700
700
|
# The filter field has been explicitly defined on the filterset class so we must manually
|
|
701
701
|
# create the new filter with the same type because there is no guarantee the defined type
|
|
702
702
|
# is the same as the default type for the field. This does not apply if the filter
|
|
@@ -727,7 +727,10 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
727
727
|
# If the base filter_field has a custom label, django_filters won't adjust it for the new_filter lookup,
|
|
728
728
|
# so we have to do it.
|
|
729
729
|
if filter_field.label and filter_field.label != label_for_filter(
|
|
730
|
-
cls.
|
|
730
|
+
cls._meta.model, # pylint: disable=no-member
|
|
731
|
+
filter_field.field_name,
|
|
732
|
+
filter_field.lookup_expr,
|
|
733
|
+
filter_field.exclude,
|
|
731
734
|
):
|
|
732
735
|
# Lightly adjusted from label_for_filter() implementation:
|
|
733
736
|
verbose_expression = ["exclude", filter_field.label] if new_filter.exclude else [filter_field.label]
|
|
@@ -750,22 +753,22 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
750
753
|
if not isinstance(new_filter_field, django_filters.Filter):
|
|
751
754
|
raise TypeError(f"Tried to add filter ({new_filter_name}) which is not an instance of Django Filter")
|
|
752
755
|
|
|
753
|
-
if new_filter_name in cls.base_filters:
|
|
756
|
+
if new_filter_name in cls.base_filters: # pylint: disable=no-member
|
|
754
757
|
raise AttributeError(
|
|
755
758
|
f"There was a conflict with filter `{new_filter_name}`, the custom filter was ignored."
|
|
756
759
|
)
|
|
757
760
|
|
|
758
|
-
cls.base_filters[new_filter_name] = new_filter_field
|
|
761
|
+
cls.base_filters[new_filter_name] = new_filter_field # pylint: disable=no-member
|
|
759
762
|
# django-filters has no concept of "abstract" filtersets, so we have to fake it
|
|
760
|
-
if cls._meta.model is not None:
|
|
761
|
-
cls.base_filters.update(
|
|
763
|
+
if cls._meta.model is not None: # pylint: disable=no-member
|
|
764
|
+
cls.base_filters.update( # pylint: disable=no-member
|
|
762
765
|
cls._generate_lookup_expression_filters(filter_name=new_filter_name, filter_field=new_filter_field)
|
|
763
766
|
)
|
|
764
767
|
|
|
765
768
|
@classmethod
|
|
766
769
|
def get_fields(cls):
|
|
767
770
|
fields = super().get_fields()
|
|
768
|
-
if "id" not in fields and (cls._meta.exclude is None or "id" not in cls._meta.exclude):
|
|
771
|
+
if "id" not in fields and (cls._meta.exclude is None or "id" not in cls._meta.exclude): # pylint: disable=no-member
|
|
769
772
|
# Add "id" as the first key in the `fields` dict
|
|
770
773
|
fields = {"id": [django_filters.conf.settings.DEFAULT_LOOKUP_EXPR], **fields}
|
|
771
774
|
return fields
|
|
@@ -782,7 +785,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
782
785
|
if filter_name.startswith("_"):
|
|
783
786
|
del filters[filter_name]
|
|
784
787
|
|
|
785
|
-
if getattr(cls._meta.model, "is_contact_associable_model", False):
|
|
788
|
+
if getattr(cls._meta.model, "is_contact_associable_model", False): # pylint: disable=no-member
|
|
786
789
|
# Add "contacts" and "teams" filters
|
|
787
790
|
from nautobot.extras.models import Contact, Team
|
|
788
791
|
|
|
@@ -802,13 +805,13 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
802
805
|
label="Teams (name or ID)",
|
|
803
806
|
)
|
|
804
807
|
|
|
805
|
-
if "dynamic_groups" not in filters and getattr(cls._meta.model, "is_dynamic_group_associable_model", False):
|
|
806
|
-
if not hasattr(cls._meta.model, "static_group_association_set"):
|
|
808
|
+
if "dynamic_groups" not in filters and getattr(cls._meta.model, "is_dynamic_group_associable_model", False): # pylint: disable=no-member
|
|
809
|
+
if not hasattr(cls._meta.model, "static_group_association_set"): # pylint: disable=no-member
|
|
807
810
|
logger.warning(
|
|
808
811
|
"Model %s has 'is_dynamic_group_associable_model = True' but lacks "
|
|
809
812
|
"a 'static_group_association_set' attribute. Perhaps this is due to it inheriting from "
|
|
810
813
|
"the deprecated DynamicGroupMixin class instead of the preferred DynamicGroupsModelMixin?",
|
|
811
|
-
cls._meta.model,
|
|
814
|
+
cls._meta.model, # pylint: disable=no-member
|
|
812
815
|
)
|
|
813
816
|
else:
|
|
814
817
|
# Add "dynamic_groups" field as the last key
|
|
@@ -818,14 +821,14 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
818
821
|
queryset=DynamicGroup.objects.all(),
|
|
819
822
|
field_name="static_group_association_set__dynamic_group",
|
|
820
823
|
to_field_name="name",
|
|
821
|
-
query_params={"content_type": cls._meta.model._meta.label_lower},
|
|
824
|
+
query_params={"content_type": cls._meta.model._meta.label_lower}, # pylint: disable=no-member
|
|
822
825
|
label="Dynamic groups (name or ID)",
|
|
823
826
|
)
|
|
824
827
|
|
|
825
828
|
# django-filters has no concept of "abstract" filtersets, so we have to fake it
|
|
826
|
-
if cls._meta.model is not None:
|
|
829
|
+
if cls._meta.model is not None: # pylint: disable=no-member
|
|
827
830
|
if "tags" in filters and isinstance(filters["tags"], TagFilter):
|
|
828
|
-
filters["tags"].extra["query_params"] = {"content_types": [cls._meta.model._meta.label_lower]}
|
|
831
|
+
filters["tags"].extra["query_params"] = {"content_types": [cls._meta.model._meta.label_lower]} # pylint: disable=no-member
|
|
829
832
|
|
|
830
833
|
new_filters = {}
|
|
831
834
|
for existing_filter_name, existing_filter in filters.items():
|
nautobot/core/forms/fields.py
CHANGED
|
@@ -102,13 +102,13 @@ class CSVFileField(django_forms.FileField):
|
|
|
102
102
|
"in double quotes."
|
|
103
103
|
)
|
|
104
104
|
|
|
105
|
-
def to_python(self,
|
|
105
|
+
def to_python(self, data):
|
|
106
106
|
"""For parity with CSVDataField, this returns the CSV text rather than an UploadedFile object."""
|
|
107
|
-
if
|
|
107
|
+
if data is None:
|
|
108
108
|
return None
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
return
|
|
110
|
+
data = super().to_python(data)
|
|
111
|
+
return data.read().decode("utf-8-sig").strip()
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
class CSVChoiceField(django_forms.ChoiceField):
|
|
@@ -774,7 +774,7 @@ class MultiMatchModelMultipleChoiceField(DynamicModelChoiceMixin, django_filters
|
|
|
774
774
|
self.natural_key = kwargs.setdefault("to_field_name", "slug")
|
|
775
775
|
super().__init__(*args, **kwargs)
|
|
776
776
|
|
|
777
|
-
def _check_values(self, values):
|
|
777
|
+
def _check_values(self, values): # pylint:disable=arguments-renamed
|
|
778
778
|
"""
|
|
779
779
|
This method overloads the grandparent method in `django.forms.models.ModelMultipleChoiceField`,
|
|
780
780
|
re-using some of that method's existing logic and adding support for coupling this field with
|
nautobot/core/graphql/types.py
CHANGED
|
@@ -10,7 +10,7 @@ class OptimizedNautobotObjectType(gql_optimizer.OptimizedDjangoObjectType):
|
|
|
10
10
|
url = graphene.String()
|
|
11
11
|
|
|
12
12
|
def resolve_url(self, info):
|
|
13
|
-
return self.get_absolute_url(api=True)
|
|
13
|
+
return self.get_absolute_url(api=True) # pylint: disable=no-member
|
|
14
14
|
|
|
15
15
|
class Meta:
|
|
16
16
|
abstract = True
|
nautobot/core/jobs/__init__.py
CHANGED
|
@@ -47,7 +47,7 @@ class GitRepositorySync(Job):
|
|
|
47
47
|
description = "Clone and/or pull a Git repository, then refresh data sourced from this repository."
|
|
48
48
|
has_sensitive_variables = False
|
|
49
49
|
|
|
50
|
-
def run(self, repository):
|
|
50
|
+
def run(self, repository): # pylint:disable=arguments-differ
|
|
51
51
|
job_result = self.job_result
|
|
52
52
|
user = job_result.user
|
|
53
53
|
|
|
@@ -88,7 +88,7 @@ class GitRepositoryDryRun(Job):
|
|
|
88
88
|
description = "Dry run of Git repository sync - will not update data sourced from this repository."
|
|
89
89
|
has_sensitive_variables = False
|
|
90
90
|
|
|
91
|
-
def run(self, repository):
|
|
91
|
+
def run(self, repository): # pylint:disable=arguments-differ
|
|
92
92
|
job_result = self.job_result
|
|
93
93
|
self.logger.info(f'Performing a Dry Run on Git repository "{repository.name}"...')
|
|
94
94
|
|
|
@@ -138,7 +138,7 @@ class ExportObjectList(Job):
|
|
|
138
138
|
soft_time_limit = 1800
|
|
139
139
|
time_limit = 2000
|
|
140
140
|
|
|
141
|
-
def run(self, *, content_type, query_string="", export_format="csv", export_template=None):
|
|
141
|
+
def run(self, *, content_type, query_string="", export_format="csv", export_template=None): # pylint:disable=arguments-differ
|
|
142
142
|
if not self.user.has_perm(f"{content_type.app_label}.view_{content_type.model}"):
|
|
143
143
|
self.logger.error('User "%s" does not have permission to view %s objects', self.user, content_type.model)
|
|
144
144
|
raise PermissionDenied("User does not have view permissions on the requested content-type")
|
|
@@ -286,7 +286,7 @@ class ImportObjects(Job):
|
|
|
286
286
|
self.logger.error("Row %d: `%s`: `%s`", row, field, err[0])
|
|
287
287
|
return new_objs, validation_failed
|
|
288
288
|
|
|
289
|
-
def run(self, *, content_type, csv_data=None, csv_file=None, roll_back_if_error=False):
|
|
289
|
+
def run(self, *, content_type, csv_data=None, csv_file=None, roll_back_if_error=False): # pylint:disable=arguments-differ
|
|
290
290
|
if not self.user.has_perm(f"{content_type.app_label}.add_{content_type.model}"):
|
|
291
291
|
self.logger.error('User "%s" does not have permission to create %s objects', self.user, content_type.model)
|
|
292
292
|
raise PermissionDenied("User does not have create permissions on the requested content-type")
|
nautobot/core/jobs/cleanup.py
CHANGED
|
@@ -70,7 +70,7 @@ class LogsCleanup(Job):
|
|
|
70
70
|
deletion_summary.update(deleted_dict)
|
|
71
71
|
return deletion_summary
|
|
72
72
|
|
|
73
|
-
def run(self, *, cleanup_types, max_age=None):
|
|
73
|
+
def run(self, *, cleanup_types, max_age=None): # pylint: disable=arguments-differ
|
|
74
74
|
if max_age in (None, ""):
|
|
75
75
|
max_age = get_settings_or_config("CHANGELOG_RETENTION")
|
|
76
76
|
if max_age == 0:
|
nautobot/core/jobs/groups.py
CHANGED
|
@@ -24,7 +24,7 @@ class RefreshDynamicGroupCaches(Job):
|
|
|
24
24
|
description = "Re-calculate and re-cache the membership lists of Dynamic Groups."
|
|
25
25
|
has_sensitive_variables = False
|
|
26
26
|
|
|
27
|
-
def run(self, single_group=None):
|
|
27
|
+
def run(self, single_group=None): # pylint: disable=arguments-differ
|
|
28
28
|
groups = DynamicGroup.objects.restrict(self.user, "view").exclude(
|
|
29
29
|
group_type=DynamicGroupTypeChoices.TYPE_STATIC
|
|
30
30
|
)
|
|
@@ -63,7 +63,7 @@ class Command(BaseCommand):
|
|
|
63
63
|
for model in models:
|
|
64
64
|
model_name = f"{model._meta.app_label}.{model.__name__}"
|
|
65
65
|
# Most swap out for user_model
|
|
66
|
-
if model_name == "auth.User":
|
|
66
|
+
if model_name == "auth.User": # pylint: disable=hard-coded-auth-user
|
|
67
67
|
model = get_user_model()
|
|
68
68
|
# Skip models that aren't actually in the database
|
|
69
69
|
if not model._meta.managed:
|
nautobot/core/models/__init__.py
CHANGED
|
@@ -270,7 +270,7 @@ class BaseModel(models.Model):
|
|
|
270
270
|
|
|
271
271
|
if not natural_key_field_names:
|
|
272
272
|
raise AttributeError(
|
|
273
|
-
f"Unable to identify an intrinsic natural-key definition for {cls.__name__}. "
|
|
273
|
+
f"Unable to identify an intrinsic natural-key definition for {cls.__name__}. " # pylint: disable=no-member
|
|
274
274
|
"If there isn't at least one UniqueConstraint, unique_together, or field with unique=True, "
|
|
275
275
|
"you probably need to explicitly declare the 'natural_key_field_names' for this model, "
|
|
276
276
|
"or potentially override the default 'natural_key_field_lookups' implementation for this model."
|
|
@@ -69,7 +69,7 @@ class JSONSet(Func):
|
|
|
69
69
|
}
|
|
70
70
|
return c
|
|
71
71
|
|
|
72
|
-
def as_sql(self, compiler, connection, function=None, **extra_context):
|
|
72
|
+
def as_sql(self, compiler, connection, function=None, **extra_context): # pylint:disable=arguments-differ
|
|
73
73
|
"""
|
|
74
74
|
MySQL implementation based on https://github.com/django/django/pull/18489/files.
|
|
75
75
|
|
|
@@ -138,7 +138,7 @@ class JSONRemove(Func):
|
|
|
138
138
|
self.path = path
|
|
139
139
|
super().__init__(expression)
|
|
140
140
|
|
|
141
|
-
def as_sql(self, compiler, connection, function=None, **extra_context):
|
|
141
|
+
def as_sql(self, compiler, connection, function=None, **extra_context): # pylint:disable=arguments-differ
|
|
142
142
|
"""
|
|
143
143
|
MySQL implementation based on https://github.com/django/django/pull/18489/files.
|
|
144
144
|
|
|
@@ -114,12 +114,12 @@ class TreeModel(TreeNode):
|
|
|
114
114
|
if self.parent_id is not None:
|
|
115
115
|
parent_display_str = cache.get(cache_key.replace(str(self.id), str(self.parent_id)), "")
|
|
116
116
|
if not parent_display_str:
|
|
117
|
-
parent_display_str = self.parent.display
|
|
117
|
+
parent_display_str = self.parent.display # pylint: disable=no-member
|
|
118
118
|
display_str = parent_display_str + " → "
|
|
119
119
|
except self.DoesNotExist:
|
|
120
120
|
# Expected to occur at times during bulk-delete operations
|
|
121
121
|
pass
|
|
122
|
-
display_str += self.name
|
|
122
|
+
display_str += self.name # pylint: disable=no-member # we checked with hasattr() above
|
|
123
123
|
cache.set(cache_key, display_str, 5)
|
|
124
124
|
return display_str
|
|
125
125
|
|
nautobot/core/tables.py
CHANGED
|
@@ -480,7 +480,7 @@ class ChoiceFieldColumn(django_tables2.Column):
|
|
|
480
480
|
choices. The CSS class is derived by calling .get_FOO_class() on the row record.
|
|
481
481
|
"""
|
|
482
482
|
|
|
483
|
-
def render(self, record, bound_column, value): # pylint: disable=arguments-differ
|
|
483
|
+
def render(self, *, record, bound_column, value): # pylint: disable=arguments-differ # tables2 varies its kwargs
|
|
484
484
|
if value:
|
|
485
485
|
name = bound_column.name
|
|
486
486
|
css_class = getattr(record, f"get_{name}_class")()
|
|
@@ -582,7 +582,7 @@ class LinkedCountColumn(django_tables2.Column):
|
|
|
582
582
|
self.model = get_model_for_view_name(self.viewname)
|
|
583
583
|
super().__init__(*args, default=default, **kwargs)
|
|
584
584
|
|
|
585
|
-
def render(self, bound_column, record, value): # pylint: disable=arguments-differ
|
|
585
|
+
def render(self, *, bound_column, record, value): # pylint: disable=arguments-differ # tables2 varies its kwargs
|
|
586
586
|
related_record = None
|
|
587
587
|
try:
|
|
588
588
|
lookup = self.lookup or get_related_field_for_models(bound_column._table._meta.model, self.model).name
|
|
@@ -666,7 +666,7 @@ class ComputedFieldColumn(django_tables2.Column):
|
|
|
666
666
|
|
|
667
667
|
super().__init__(*args, **kwargs)
|
|
668
668
|
|
|
669
|
-
def render(self, record):
|
|
669
|
+
def render(self, *, record): # pylint: disable=arguments-differ # tables2 varies its kwargs
|
|
670
670
|
return self.computedfield.render({"obj": record})
|
|
671
671
|
|
|
672
672
|
|
|
@@ -685,7 +685,7 @@ class CustomFieldColumn(django_tables2.Column):
|
|
|
685
685
|
|
|
686
686
|
super().__init__(*args, **kwargs)
|
|
687
687
|
|
|
688
|
-
def render(self, record, bound_column, value): # pylint: disable=arguments-differ
|
|
688
|
+
def render(self, *, record, bound_column, value): # pylint: disable=arguments-differ # tables2 varies its kwargs
|
|
689
689
|
if self.customfield.type == choices.CustomFieldTypeChoices.TYPE_BOOLEAN:
|
|
690
690
|
template = helpers.render_boolean(value)
|
|
691
691
|
elif self.customfield.type == choices.CustomFieldTypeChoices.TYPE_MULTISELECT:
|
|
@@ -716,7 +716,7 @@ class RelationshipColumn(django_tables2.Column):
|
|
|
716
716
|
kwargs.setdefault("accessor", Accessor("associations"))
|
|
717
717
|
super().__init__(orderable=False, *args, **kwargs)
|
|
718
718
|
|
|
719
|
-
def render(self, record, value): # pylint: disable=arguments-differ
|
|
719
|
+
def render(self, *, record, value): # pylint: disable=arguments-differ # tables2 varies its kwargs
|
|
720
720
|
# Filter the relationship associations by the relationship instance.
|
|
721
721
|
# Since associations accessor returns all the relationship associations regardless of the relationship.
|
|
722
722
|
value = [v for v in value if v.relationship == self.relationship]
|
nautobot/core/testing/filters.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations # python 3.8
|
|
2
|
+
|
|
1
3
|
import random
|
|
2
4
|
import string
|
|
3
5
|
|
|
4
6
|
from django.contrib.contenttypes.models import ContentType
|
|
5
|
-
from django.db.models import Count, Q
|
|
7
|
+
from django.db.models import Count, Q, QuerySet
|
|
6
8
|
from django.db.models.fields import CharField, TextField
|
|
7
9
|
from django.db.models.fields.related import ManyToManyField
|
|
8
10
|
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
|
|
9
11
|
from django.test import tag
|
|
12
|
+
from django_filters import FilterSet
|
|
10
13
|
|
|
11
14
|
from nautobot.core.constants import CHARFIELD_MAX_LENGTH
|
|
12
15
|
from nautobot.core.filters import (
|
|
@@ -27,6 +30,8 @@ class FilterTestCases:
|
|
|
27
30
|
class BaseFilterTestCase(views.TestCase):
|
|
28
31
|
"""Base class for testing of FilterSets."""
|
|
29
32
|
|
|
33
|
+
queryset: QuerySet
|
|
34
|
+
|
|
30
35
|
def get_filterset_test_values(self, field_name, queryset=None):
|
|
31
36
|
"""Returns a list of distinct values from the requested queryset field to use in filterset tests.
|
|
32
37
|
|
|
@@ -68,8 +73,7 @@ class FilterTestCases:
|
|
|
68
73
|
class FilterTestCase(BaseFilterTestCase):
|
|
69
74
|
"""Add common tests for all FilterSets."""
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
filterset = None
|
|
76
|
+
filterset: type[FilterSet]
|
|
73
77
|
|
|
74
78
|
# filter predicate fields that should be excluded from q test case
|
|
75
79
|
exclude_q_filter_predicates = []
|
nautobot/core/testing/views.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations # python 3.8
|
|
2
|
+
|
|
1
3
|
import contextlib
|
|
2
4
|
import re
|
|
3
5
|
from typing import Optional, Sequence
|
|
@@ -21,6 +23,7 @@ from nautobot.core.models.tree_queries import TreeModel
|
|
|
21
23
|
from nautobot.core.templatetags import helpers
|
|
22
24
|
from nautobot.core.testing import mixins, utils
|
|
23
25
|
from nautobot.core.utils import lookup
|
|
26
|
+
from nautobot.dcim.models.device_components import ComponentModel
|
|
24
27
|
from nautobot.extras import choices as extras_choices, models as extras_models, querysets as extras_querysets
|
|
25
28
|
from nautobot.extras.forms import CustomFieldModelFormMixin, RelationshipModelFormMixin
|
|
26
29
|
from nautobot.extras.models import CustomFieldModel, RelationshipModel
|
|
@@ -1528,6 +1531,8 @@ class ViewTestCases:
|
|
|
1528
1531
|
maxDiff = None
|
|
1529
1532
|
bulk_add_data = None
|
|
1530
1533
|
"""Used for bulk-add (distinct from bulk-create) view testing; self.bulk_create_data will be used if unset."""
|
|
1534
|
+
selected_objects: list[ComponentModel]
|
|
1535
|
+
selected_objects_parent_name: str
|
|
1531
1536
|
|
|
1532
1537
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1533
1538
|
def test_bulk_add_component(self):
|
nautobot/core/tests/runner.py
CHANGED
|
@@ -142,7 +142,7 @@ class NautobotTestRunner(DiscoverRunner):
|
|
|
142
142
|
# branches/releases of Nautobot in separate files.
|
|
143
143
|
hexdigest = hashlib.shake_128(
|
|
144
144
|
",".join(
|
|
145
|
-
sorted(f"{m.app}.{m.name}" for m in MigrationRecorder.Migration.objects.all())
|
|
145
|
+
sorted(f"{m.app}.{m.name}" for m in MigrationRecorder.Migration.objects.all()) # pylint: disable=no-member
|
|
146
146
|
).encode("utf-8")
|
|
147
147
|
).hexdigest(10)
|
|
148
148
|
command += ["--fixture-file", f"development/factory_dump.{hexdigest}.json"]
|