瀏覽代碼

feat(users): Add support for cloning ObjectPermission objects

Introduces cloning functionality for ObjectPermission objects using the
CloningMixin. Updates the constraints field handling, adds JSONField,
and introduces logic to process initial data for cloned objects.

Fixes #15492
Martin Hauser 5 月之前
父節點
當前提交
34b111bdc4
共有 2 個文件被更改,包括 40 次插入13 次删除
  1. 34 12
      netbox/users/forms/model_forms.py
  2. 6 1
      netbox/users/models/permissions.py

+ 34 - 12
netbox/users/forms/model_forms.py

@@ -1,3 +1,5 @@
+import json
+
 from django import forms
 from django import forms
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth import password_validation
 from django.contrib.auth import password_validation
@@ -13,7 +15,11 @@ from netbox.preferences import PREFERENCES
 from users.constants import *
 from users.constants import *
 from users.models import *
 from users.models import *
 from utilities.data import flatten_dict
 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.rendering import FieldSet
 from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget
 from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget
 from utilities.permissions import qs_filter_from_constraints
 from utilities.permissions import qs_filter_from_constraints
@@ -316,13 +322,22 @@ class ObjectPermissionForm(forms.ModelForm):
         required=False,
         required=False,
         queryset=Group.objects.all()
         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 = (
     fieldsets = (
         FieldSet('name', 'description', 'enabled'),
         FieldSet('name', 'description', 'enabled'),
         FieldSet('can_view', 'can_add', 'can_change', 'can_delete', 'actions', name=_('Actions')),
         FieldSet('can_view', 'can_add', 'can_change', 'can_delete', 'actions', name=_('Actions')),
         FieldSet('object_types', name=_('Objects')),
         FieldSet('object_types', name=_('Objects')),
         FieldSet('groups', 'users', name=_('Assignment')),
         FieldSet('groups', 'users', name=_('Assignment')),
-        FieldSet('constraints', name=_('Constraints'))
+        FieldSet('constraints', name=_('Constraints')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -330,13 +345,6 @@ class ObjectPermissionForm(forms.ModelForm):
         fields = [
         fields = [
             'name', 'description', 'enabled', 'object_types', 'users', 'groups', 'constraints', 'actions',
             '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):
     def __init__(self, *args, **kwargs):
         super().__init__(*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
         # Make the actions field optional since the form uses it only for non-CRUD actions
         self.fields['actions'].required = False
         self.fields['actions'].required = False
 
 
-        # Populate assigned users and groups
+        # Prepare the appropriate fields when editing an existing ObjectPermission
         if self.instance.pk:
         if self.instance.pk:
+            # Populate assigned users and groups
             self.fields['groups'].initial = self.instance.groups.values_list('id', flat=True)
             self.fields['groups'].initial = self.instance.groups.values_list('id', flat=True)
             self.fields['users'].initial = self.instance.users.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']:
             for action in ['view', 'add', 'change', 'delete']:
                 if action in self.instance.actions:
                 if action in self.instance.actions:
                     self.fields[f'can_{action}'].initial = True
                     self.fields[f'can_{action}'].initial = True
                     self.instance.actions.remove(action)
                     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):
     def clean(self):
         super().clean()
         super().clean()
 
 

+ 6 - 1
netbox/users/models/permissions.py

@@ -3,6 +3,7 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
+from netbox.models.features import CloningMixin
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
 
 
 __all__ = (
 __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
     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.
     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)")
         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()
     objects = RestrictedQuerySet.as_manager()
 
 
     class Meta:
     class Meta: