diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 505104c03..3e8c853a5 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -1,3 +1,5 @@ +import json + from django import forms from django.conf import settings from django.contrib.auth import password_validation @@ -13,7 +15,11 @@ from netbox.preferences import PREFERENCES from users.constants import * from users.models import * from utilities.data import flatten_dict -from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import ( + ContentTypeMultipleChoiceField, + DynamicModelMultipleChoiceField, + JSONField, +) from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget from utilities.permissions import qs_filter_from_constraints @@ -316,13 +322,22 @@ class ObjectPermissionForm(forms.ModelForm): required=False, queryset=Group.objects.all() ) + constraints = JSONField( + required=False, + label=_('Constraints'), + help_text=_( + 'JSON expression of a queryset filter that will return only permitted objects. Leave null ' + 'to match all objects of this type. A list of multiple objects will result in a logical OR ' + 'operation.' + ), + ) fieldsets = ( FieldSet('name', 'description', 'enabled'), FieldSet('can_view', 'can_add', 'can_change', 'can_delete', 'actions', name=_('Actions')), FieldSet('object_types', name=_('Objects')), FieldSet('groups', 'users', name=_('Assignment')), - FieldSet('constraints', name=_('Constraints')) + FieldSet('constraints', name=_('Constraints')), ) class Meta: @@ -330,13 +345,6 @@ class ObjectPermissionForm(forms.ModelForm): fields = [ 'name', 'description', 'enabled', 'object_types', 'users', 'groups', 'constraints', 'actions', ] - help_texts = { - 'constraints': _( - 'JSON expression of a queryset filter that will return only permitted objects. Leave null ' - 'to match all objects of this type. A list of multiple objects will result in a logical OR ' - 'operation.' - ) - } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -344,18 +352,32 @@ class ObjectPermissionForm(forms.ModelForm): # Make the actions field optional since the form uses it only for non-CRUD actions self.fields['actions'].required = False - # Populate assigned users and groups + # Prepare the appropriate fields when editing an existing ObjectPermission if self.instance.pk: + # Populate assigned users and groups self.fields['groups'].initial = self.instance.groups.values_list('id', flat=True) self.fields['users'].initial = self.instance.users.values_list('id', flat=True) - # Check the appropriate checkboxes when editing an existing ObjectPermission - if self.instance.pk: + # Check the appropriate checkboxes when editing an existing ObjectPermission for action in ['view', 'add', 'change', 'delete']: if action in self.instance.actions: self.fields[f'can_{action}'].initial = True self.instance.actions.remove(action) + # Populate initial data for a new ObjectPermission + elif self.initial: + # Handle cloned objects - actions come from initial data (URL parameters) + if 'actions' in self.initial: + if cloned_actions := self.initial['actions']: + for action in ['view', 'add', 'change', 'delete']: + if action in cloned_actions: + self.fields[f'can_{action}'].initial = True + self.initial['actions'].remove(action) + # Convert data delivered via initial data to JSON data + if 'constraints' in self.initial: + if type(self.initial['constraints']) is str: + self.initial['constraints'] = json.loads(self.initial['constraints']) + def clean(self): super().clean() diff --git a/netbox/users/models/permissions.py b/netbox/users/models/permissions.py index 3ae8ff4c1..3e41d4356 100644 --- a/netbox/users/models/permissions.py +++ b/netbox/users/models/permissions.py @@ -3,6 +3,7 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from netbox.models.features import CloningMixin from utilities.querysets import RestrictedQuerySet __all__ = ( @@ -10,7 +11,7 @@ __all__ = ( ) -class ObjectPermission(models.Model): +class ObjectPermission(CloningMixin, models.Model): """ A mapping of view, add, change, and/or delete permission for users and/or groups to an arbitrary set of objects identified by ORM query parameters. @@ -43,6 +44,10 @@ class ObjectPermission(models.Model): help_text=_("Queryset filter matching the applicable objects of the selected type(s)") ) + clone_fields = ( + 'description', 'enabled', 'object_types', 'actions', 'constraints', + ) + objects = RestrictedQuerySet.as_manager() class Meta: