Skip to content

Commit 34b111b

Browse files
pheusjeremystretch
authored andcommitted
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
1 parent 6841060 commit 34b111b

File tree

2 files changed

+40
-13
lines changed

2 files changed

+40
-13
lines changed

netbox/users/forms/model_forms.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
from django import forms
24
from django.conf import settings
35
from django.contrib.auth import password_validation
@@ -13,7 +15,11 @@
1315
from users.constants import *
1416
from users.models import *
1517
from utilities.data import flatten_dict
16-
from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField
18+
from utilities.forms.fields import (
19+
ContentTypeMultipleChoiceField,
20+
DynamicModelMultipleChoiceField,
21+
JSONField,
22+
)
1723
from utilities.forms.rendering import FieldSet
1824
from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget
1925
from utilities.permissions import qs_filter_from_constraints
@@ -316,46 +322,62 @@ class ObjectPermissionForm(forms.ModelForm):
316322
required=False,
317323
queryset=Group.objects.all()
318324
)
325+
constraints = JSONField(
326+
required=False,
327+
label=_('Constraints'),
328+
help_text=_(
329+
'JSON expression of a queryset filter that will return only permitted objects. Leave null '
330+
'to match all objects of this type. A list of multiple objects will result in a logical OR '
331+
'operation.'
332+
),
333+
)
319334

320335
fieldsets = (
321336
FieldSet('name', 'description', 'enabled'),
322337
FieldSet('can_view', 'can_add', 'can_change', 'can_delete', 'actions', name=_('Actions')),
323338
FieldSet('object_types', name=_('Objects')),
324339
FieldSet('groups', 'users', name=_('Assignment')),
325-
FieldSet('constraints', name=_('Constraints'))
340+
FieldSet('constraints', name=_('Constraints')),
326341
)
327342

328343
class Meta:
329344
model = ObjectPermission
330345
fields = [
331346
'name', 'description', 'enabled', 'object_types', 'users', 'groups', 'constraints', 'actions',
332347
]
333-
help_texts = {
334-
'constraints': _(
335-
'JSON expression of a queryset filter that will return only permitted objects. Leave null '
336-
'to match all objects of this type. A list of multiple objects will result in a logical OR '
337-
'operation.'
338-
)
339-
}
340348

341349
def __init__(self, *args, **kwargs):
342350
super().__init__(*args, **kwargs)
343351

344352
# Make the actions field optional since the form uses it only for non-CRUD actions
345353
self.fields['actions'].required = False
346354

347-
# Populate assigned users and groups
355+
# Prepare the appropriate fields when editing an existing ObjectPermission
348356
if self.instance.pk:
357+
# Populate assigned users and groups
349358
self.fields['groups'].initial = self.instance.groups.values_list('id', flat=True)
350359
self.fields['users'].initial = self.instance.users.values_list('id', flat=True)
351360

352-
# Check the appropriate checkboxes when editing an existing ObjectPermission
353-
if self.instance.pk:
361+
# Check the appropriate checkboxes when editing an existing ObjectPermission
354362
for action in ['view', 'add', 'change', 'delete']:
355363
if action in self.instance.actions:
356364
self.fields[f'can_{action}'].initial = True
357365
self.instance.actions.remove(action)
358366

367+
# Populate initial data for a new ObjectPermission
368+
elif self.initial:
369+
# Handle cloned objects - actions come from initial data (URL parameters)
370+
if 'actions' in self.initial:
371+
if cloned_actions := self.initial['actions']:
372+
for action in ['view', 'add', 'change', 'delete']:
373+
if action in cloned_actions:
374+
self.fields[f'can_{action}'].initial = True
375+
self.initial['actions'].remove(action)
376+
# Convert data delivered via initial data to JSON data
377+
if 'constraints' in self.initial:
378+
if type(self.initial['constraints']) is str:
379+
self.initial['constraints'] = json.loads(self.initial['constraints'])
380+
359381
def clean(self):
360382
super().clean()
361383

netbox/users/models/permissions.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
from django.urls import reverse
44
from django.utils.translation import gettext_lazy as _
55

6+
from netbox.models.features import CloningMixin
67
from utilities.querysets import RestrictedQuerySet
78

89
__all__ = (
910
'ObjectPermission',
1011
)
1112

1213

13-
class ObjectPermission(models.Model):
14+
class ObjectPermission(CloningMixin, models.Model):
1415
"""
1516
A mapping of view, add, change, and/or delete permission for users and/or groups to an arbitrary set of objects
1617
identified by ORM query parameters.
@@ -43,6 +44,10 @@ class ObjectPermission(models.Model):
4344
help_text=_("Queryset filter matching the applicable objects of the selected type(s)")
4445
)
4546

47+
clone_fields = (
48+
'description', 'enabled', 'object_types', 'actions', 'constraints',
49+
)
50+
4651
objects = RestrictedQuerySet.as_manager()
4752

4853
class Meta:

0 commit comments

Comments
 (0)