{% trans "Region" %} | -- {% if object.site.region %} - {% for region in object.site.region.get_ancestors %} - {{ region|linkify }} / - {% endfor %} - {{ object.site.region|linkify }} - {% else %} - {{ ''|placeholder }} - {% endif %} - | +{% nested_tree object.site.region %} | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{% trans "Site" %} | @@ -32,16 +24,7 @@|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{% trans "Location" %} | -- {% if object.location %} - {% for location in object.location.get_ancestors %} - {{ location|linkify }} / - {% endfor %} - {{ object.location|linkify }} - {% else %} - {{ ''|placeholder }} - {% endif %} - | +{% nested_tree object.location %} | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{% trans "Rack" %} | diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 671c7ab2e..857061d00 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -4,6 +4,7 @@ {% load static %} {% load plugins %} {% load i18n %} +{% load mptt %} {% block content %}
{% trans "Site" %} | +{% trans "Region" %} | - {% if object.site.region %} - {{ object.site.region|linkify }} / - {% endif %} - {{ object.site|linkify }} + {% nested_tree object.site.region %} | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{% trans "Site" %} | +{{ object.site|linkify }} | +|||||||||||||||||
{% trans "Location" %} | -- {% if object.location %} - {% for location in object.location.get_ancestors %} - {{ location|linkify }} / - {% endfor %} - {{ object.location|linkify }} - {% else %} - {{ ''|placeholder }} - {% endif %} - | +{% nested_tree object.location %} | ||||||||||||||||
{% trans "Facility ID" %} | diff --git a/netbox/templates/dcim/rackreservation.html b/netbox/templates/dcim/rackreservation.html index 8edb75f32..3d145145f 100644 --- a/netbox/templates/dcim/rackreservation.html +++ b/netbox/templates/dcim/rackreservation.html @@ -4,6 +4,7 @@ {% load static %} {% load plugins %} {% load i18n %} +{% load mptt %} {% block breadcrumbs %} {{ block.super }} @@ -20,25 +21,24 @@
{% trans "Site" %} | -- {% if rack.site.region %} - {{ rack.site.region|linkify }} / - {% endif %} - {{ rack.site|linkify }} - | -
---|---|
{% trans "Location" %} | -{{ rack.location|linkify|placeholder }} | -
{% trans "Rack" %} | -{{ rack|linkify }} | -
{% trans "Region" %} | ++ {% nested_tree object.rack.site.region %} + | +
{% trans "Site" %} | +{{ object.rack.site|linkify }} | +
{% trans "Location" %} | +{{ object.rack.location|linkify|placeholder }} | +
{% trans "Rack" %} | +{{ object.rack|linkify }} | +
{% trans "Name" %} | +{{ object.name }} | +
---|---|
{% trans "Enabled" %} | +{% checkmark object.enabled %} | +
{% trans "Description" %} | +{{ object.description|placeholder }} | +
{% trans "Create" %} | +{% checkmark object.type_create %} | +
---|---|
{% trans "Update" %} | +{% checkmark object.type_update %} | +
{% trans "Delete" %} | +{% checkmark object.type_delete %} | +
{% trans "Job start" %} | +{% checkmark object.type_job_start %} | +
{% trans "Job end" %} | +{% checkmark object.type_job_end %} | +
{{ ct }} | +
{{ object.conditions|json }}+ {% else %} +
{% trans "None" %}
+ {% endif %} +{% trans "Create" %} | -{% checkmark object.type_create %} | -
---|---|
{% trans "Update" %} | -{% checkmark object.type_update %} | -
{% trans "Delete" %} | -{% checkmark object.type_delete %} | -
{% trans "Job start" %} | -{% checkmark object.type_job_start %} | -
{% trans "Job end" %} | -{% checkmark object.type_job_end %} | -
{{ ct }} | -
{{ object.conditions|json }}- {% else %} -
{% trans "None" %}
- {% endif %} -{% trans "Region" %} | ++ {% nested_tree object.site.region %} + | +|
---|---|---|
{% trans "Site" %} | -- {% if object.site %} - {% if object.site.region %} - {{ object.site.region|linkify }} / - {% endif %} - {{ object.site|linkify }} - {% else %} - {{ ''|placeholder }} - {% endif %} - | +{{ object.site|linkify|placeholder }} |
{% trans "Group" %} | diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index e7f319051..28bf92958 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from core.models import ContentType from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel -from netbox.models.features import CustomFieldsMixin, TagsMixin +from netbox.models.features import CustomFieldsMixin, ExportTemplatesMixin, TagsMixin from tenancy.choices import * __all__ = ( @@ -110,7 +110,7 @@ class Contact(PrimaryModel): return reverse('tenancy:contact', args=[self.pk]) -class ContactAssignment(CustomFieldsMixin, TagsMixin, ChangeLoggedModel): +class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): content_type = models.ForeignKey( to='contenttypes.ContentType', on_delete=models.CASCADE diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 75ab877cf..c9775e39a 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -52,6 +52,16 @@ class UserSerializer(ValidatedModelSerializer): return user + def update(self, instance, validated_data): + """ + Ensure proper updated password hash generation. + """ + password = validated_data.pop('password', None) + if password is not None: + instance.set_password(password) + + return super().update(instance, validated_data) + @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj): if full_name := obj.get_full_name(): diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 001142410..090ccc263 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -54,6 +54,38 @@ class UserTest(APIViewTestCases.APIViewTestCase): ) User.objects.bulk_create(users) + def test_that_password_is_changed(self): + """ + Test that password is changed + """ + + obj_perm = ObjectPermission( + name='Test permission', + actions=['change'] + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + + user_credentials = { + 'username': 'user1', + 'password': 'abc123', + } + user = User.objects.create_user(**user_credentials) + + data = { + 'password': 'newpassword' + } + url = reverse('users-api:user-detail', kwargs={'pk': user.id}) + + response = self.client.patch(url, data, format='json', **self.header) + + self.assertEqual(response.status_code, 200) + + updated_user = User.objects.get(id=user.id) + + self.assertTrue(updated_user.check_password(data['password'])) + class GroupTest(APIViewTestCases.APIViewTestCase): model = Group diff --git a/netbox/utilities/forms/fields/fields.py b/netbox/utilities/forms/fields/fields.py index db5e4a30d..d4d4ae19b 100644 --- a/netbox/utilities/forms/fields/fields.py +++ b/netbox/utilities/forms/fields/fields.py @@ -103,7 +103,7 @@ class JSONField(_JSONField): def prepare_value(self, value): if isinstance(value, InvalidJSONInput): return value - if value is None: + if value in ('', None): return '' return json.dumps(value, sort_keys=True, indent=4) diff --git a/netbox/utilities/forms/utils.py b/netbox/utilities/forms/utils.py index 4d737f163..de8e22727 100644 --- a/netbox/utilities/forms/utils.py +++ b/netbox/utilities/forms/utils.py @@ -40,7 +40,7 @@ def parse_numeric_range(string, base=10): except ValueError: raise forms.ValidationError(f'Range "{dash_range}" is invalid.') values.extend(range(begin, end)) - return list(set(values)) + return sorted(set(values)) def parse_alphanumeric_range(string): @@ -128,10 +128,9 @@ def get_field_value(form, field_name): """ field = form.fields[field_name] - if form.is_bound: - if data := form.data.get(field_name): - if field.valid_value(data): - return data + if form.is_bound and (data := form.data.get(field_name)): + if hasattr(field, 'valid_value') and field.valid_value(data): + return data return form.get_initial_for_field(field, field_name) diff --git a/netbox/utilities/templatetags/mptt.py b/netbox/utilities/templatetags/mptt.py new file mode 100644 index 000000000..783c2654f --- /dev/null +++ b/netbox/utilities/templatetags/mptt.py @@ -0,0 +1,20 @@ +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + + +@register.simple_tag() +def nested_tree(obj): + """ + Renders the entire hierarchy of a recursively-nested object (such as Region or SiteGroup). + """ + if not obj: + return mark_safe('—') + + nodes = obj.get_ancestors(include_self=True) + return mark_safe( + ' / '.join( + f'{node}' for node in nodes + ) + ) diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 72990ec76..b76d8a160 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -296,9 +296,10 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): # Check interface sites. First interface should set site, further interfaces will either continue the # loop or reset back to no site and break the loop. for interface in interfaces: + vm_site = interface.virtual_machine.site or interface.virtual_machine.cluster.site if site is None: - site = interface.virtual_machine.cluster.site - elif interface.virtual_machine.cluster.site is not site: + site = vm_site + elif vm_site is not site: site = None break diff --git a/requirements.txt b/requirements.txt index 45fb12f80..537c5b77e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,18 @@ bleach==6.1.0 Django==4.2.7 -django-cors-headers==4.3.0 +django-cors-headers==4.3.1 django-debug-toolbar==4.2.0 -django-filter==23.3 +django-filter==23.4 django-graphiql-debug-toolbar==0.2.0 django-mptt==0.14.0 django-pglocks==1.0.4 django-prometheus==2.3.1 django-redis==5.4.0 django-rich==1.8.0 -django-rq==2.8.1 +django-rq==2.9.0 django-tables2==2.6.0 django-taggit==4.0.0 -django-timezone-field==6.0.1 +django-timezone-field==6.1.0 djangorestframework==3.14.0 drf-spectacular==0.26.5 drf-spectacular-sidecar==2023.10.1 @@ -21,15 +21,15 @@ graphene-django==3.0.0 gunicorn==21.2.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.4.8 -mkdocstrings[python-legacy]==0.23.0 +mkdocs-material==9.4.14 +mkdocstrings[python-legacy]==0.24.0 netaddr==0.9.0 Pillow==10.1.0 -psycopg[binary,pool]==3.1.12 +psycopg[binary,pool]==3.1.13 PyYAML==6.0.1 requests==2.31.0 social-auth-app-django==5.4.0 -social-auth-core[openidconnect]==4.5.0 +social-auth-core[openidconnect]==4.5.1 svgwrite==1.4.3 tablib==3.5.0 tzdata==2023.3