From 5b18d88db0bb9111c5efa89eb08d3dd3d077f939 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Jul 2025 11:18:23 -0400 Subject: [PATCH] Closes #19968: Use multiple selection lists for the assignment of object types when editing a permission --- netbox/project-static/dist/netbox.js | Bin 382185 -> 382598 bytes netbox/project-static/dist/netbox.js.map | Bin 1733459 -> 1735154 bytes .../project-static/src/buttons/moveOptions.ts | 52 +++++++++--- netbox/templates/extras/tableconfig_edit.html | 4 +- netbox/users/forms/model_forms.py | 11 ++- netbox/utilities/forms/widgets/select.py | 77 ++++++++++++++++++ .../templates/helpers/table_config_form.html | 8 +- .../templates/widgets/splitmultiselect.html | 31 +++++++ 8 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 netbox/utilities/templates/widgets/splitmultiselect.html diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 9e2d6b383c2f22e003900f3bd894ce5d117a9676..2877acb588c9a599adaa5ffffd96608093741e88 100644 GIT binary patch delta 1029 zcmZuwU1$_n6z0sCn;21|v57SivTlg+lYtmikosU>$mw6yrocl79GTt}ffwkr0%rP+qB>-lGr+>)erEnLnb(w`0M z`e52~Pk0VD9naESJx1RbuwmmfGO_Y3*JCAn=xQA6o0jSQ?@l1)ynqpWNMl1-2PRz^ z!Z+!MI5yOlb$DUPZV?{FY6bStt0Oo7N9pDktQU7i@H(hREy*-`4m)9wSojnR3Umnj zJiZRlPG5e8Md+Z{M)7=YyQKw2x1;B!75#3yWPT_qei_AkuqIj3y9_62Y7BEqr$rYE z*iD}l@N3vX&EvSeqSMlJBSG15sbB}49mgeTqLULCfo*hU0;8}`{5pYKVdb_`OJkp0 z>8B#LQdbduI6`I-U!lK>I3ddhzQBvhev4p84r_WGE2(V~TVa5DCox)cMCvM^S~*eguij;qIj}WNkMdAQi^EVUB7LfH4!XsGC6!mmIQ_3$ zHLrA8boBuX)4MaQfmY10b;{0VBmxJ-u^Hw9bkT3OSWm@H>04f>wt3b_2XC`eGXMK+ zW-AKMQQ|K9qZGAS7C;~_&9X7533AfEGDdFh>2o4A$5tq7+?@Pb%LB9N5rbcmuuhYm@dtV7YaqFA?15AXBvywCeS1NU?vr**yk za9{Z3gqp#1r@+^pIDddleV8D9A6n^MAKuM1UJXX1;$WBJ3kHhAU4`_16!#V>?vN}g zg%-QdYnBWZ6@jqdZ}A30X1}E96@AdFuQRtBO+8Yqoz|jgfD>ehV?G$EERLnd|Fw}m zj+=NojyVD}(8?f2;S4=?U;)2;9Up=^7>4j1H1O~c-V~sdYlg7|pplM_UhfD+mY6pj@MIp**YWNIEFGn}Nn6c&Sts#9o#Dz2pPIOLl$5!R}IgPO@bfks*%#}Lhr zqlx+_FsUxk5#AO$WV*J*wo%&{ZlYzvgV0Pr2yNP^Y_b0-IiQw&p@Uc6!Wq-n7P;cE z&1V1mUiX7XSgwT@ZePKHVd&j_7$_x(?zzA?#!`L&Dr0?^0G>Nn`6CD&!f+oPY_yV`Y_MlW-z_L6p(oN zB1?c)im1h9b*)5xW%KV#Y_pK7M9kS@fbv$Eyd@CHGWa6&#DP0$(jziHy~?ZrK6$`PRe diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 0ba7a62d196c16b438f603fcc2e4ccad2acccbf2..b6ebd63dd1104ad24fdc4e147817ff6a94a54fcf 100644 GIT binary patch delta 2083 zcma)7O>7%Q6jq>VgST!g@O@ZtEE1gy4ddY@JQIt+Q+F zbrYx{94u8F_>rhICk}`!QYD0BiCa}Du^kBz;s^&0oH&8Rsr<~F*==lyMJ>JcYTkR_ z`@Zju?C*aufBW5hyVg^Cr1ofSU#+*czjmP3S9`4H`&>Er<5Dmm8ke9L$+F(Bl@N{Ud{W&QOCuyOk%GFJ(+(YJgjdu8YH{=PvL zY59XaHoMgFkFvjS`TX@i?kb<`^GN{{>-Ke)y`wzM8r#aq*u(@%+t)pH#tM?CW0f$v zY&aW+VWFZ~aVqSmZDs1I2yZq{%aW7fFuK8I^H0IHDvl?6rlE|o557@`*mDgf8fecg zNeV3cuJ1_yvbnN!{D$=6&E+tAZ)G6Je!SD)=ND3(J&I+*E5-$h7&^*^_qIKeqWatk;~$RfpYLZX`Z zGAX8nPQ`InBrXuLfYXGeF@L*|fWi#Uz#~rRCXLI3MMCFe@-qDKIY2?GG0R?Qs^;0fXE++vq})N=p9Kz1Ee)smDa;_5^MH8dMcKb6 z4U4*!LIB<+lLn?KV7f^~e^w;ngrw)#wU&BxD+8l8z>~N`+g2bPJ*D9SAqA*U3Sx1n zPQMHXe3L*=Aj$_gLgz&;w`WeUU(>`rzy|jQr?)uVC7>xJKmm1PI&^o|{kLp0wg_ad zX>eKi+Prr>(ch`6&1_-ik{@-o+hmrNHhcL${=K%r;)3Qc(6_&Iv3SWTRq{AKJUHl=WIdT4) zWQiShJo^$*$nCuyasU6{^?U~p_SKFWQg?@K!Z6Csq*oI+@9(HQp5QTDzrQo`p)q(2 pqhs=42HxZk5c@P59A@uFgCX`sG?*BcdH0uew`{5II}=Qu`v=a-qFVp} delta 908 zcmcIjO=uHA7$vqQW~Zq?jZ+g#yEcNRPALH|sUn?BHru2Lp~esb9-3;@v~?5OjTMUE zVWAhL2M^yt4_-YAO7P%8&`Y67YA&9NUc41i!JFWGyMcJT<9`3$>bou5ZK>V6bN>R>JK4(vRX4MRb#SVI~NYC8!)p&u6QfA)bnO(^>!J)zg8$@ zzPS|GYx>+U_(ci%7eZ$@>|67}0`^=tR23pkGVNU}uas|?;qWCWUF;i%PkTOJ&(9Tt zUrmzx7x8y3a@o@rhb>S>Hb>h;VnD(tF(`3V;+Vv7i4zh-68^?$`{e#_@2bXGj`Lg| zo;T@IS};u>61dI_COF4}sKxAhmKC_THfvw9BIiXN85v|mV%>6y^O9~eW?`yLDXg?; zz@ssp^KuFvwPNcRy9=2rcv?qRj2>J7>4LFos7LO8YWF* z()%LayPU&qCIQddbZkgRtMwS#nu5)9UJ$UeOUH_`ocSwsmZ7e*NWnXCJGh431vbaU zqSGt4Ja13(g7XM_o;WRzAw@KvfUR9BAB6p-f-_5dJ4wV>|IMrD01fG;(A{sp7cK!;`qrsHoeCy*(9dEpd29q;?063!` AY5)KL diff --git a/netbox/project-static/src/buttons/moveOptions.ts b/netbox/project-static/src/buttons/moveOptions.ts index fee36d609..f937530b4 100644 --- a/netbox/project-static/src/buttons/moveOptions.ts +++ b/netbox/project-static/src/buttons/moveOptions.ts @@ -1,5 +1,20 @@ import { getElements } from '../util'; +/** + * Move selected options from one select element to another. + * + * @param source Select Element + * @param target Select Element + */ +function moveOption(source: HTMLSelectElement, target: HTMLSelectElement): void { + for (const option of Array.from(source.options)) { + if (option.selected) { + target.appendChild(option.cloneNode(true)); + option.remove(); + } + } +} + /** * Move selected options of a select element up in order. * @@ -39,23 +54,38 @@ function moveOptionDown(element: HTMLSelectElement): void { } /** - * Initialize move up/down buttons. + * Initialize select/move buttons. */ export function initMoveButtons(): void { - for (const button of getElements('#move-option-up')) { + // Move selected option(s) between lists + for (const button of getElements('.move-option')) { + const source = button.getAttribute('data-source'); const target = button.getAttribute('data-target'); - if (target !== null) { - for (const select of getElements(`#${target}`)) { - button.addEventListener('click', () => moveOptionUp(select)); - } + const source_select = document.getElementById(`id_${source}`) as HTMLSelectElement; + const target_select = document.getElementById(`id_${target}`) as HTMLSelectElement; + if (source_select !== null && target_select !== null) { + button.addEventListener('click', () => moveOption(source_select, target_select)); } } - for (const button of getElements('#move-option-down')) { + + // Move selected option(s) up in current list + for (const button of getElements('.move-option-up')) { + console.log(button); const target = button.getAttribute('data-target'); - if (target !== null) { - for (const select of getElements(`#${target}`)) { - button.addEventListener('click', () => moveOptionDown(select)); - } + console.log(target); + const target_select = document.getElementById(`id_${target}`) as HTMLSelectElement; + console.log(target_select); + if (target_select !== null) { + button.addEventListener('click', () => moveOptionUp(target_select)); + } + } + + // Move selected option(s) down in current list + for (const button of getElements('.move-option-down')) { + const target = button.getAttribute('data-target'); + const target_select = document.getElementById(`id_${target}`) as HTMLSelectElement; + if (target_select !== null) { + button.addEventListener('click', () => moveOptionDown(target_select)); } } } diff --git a/netbox/templates/extras/tableconfig_edit.html b/netbox/templates/extras/tableconfig_edit.html index df1e67082..31057c298 100644 --- a/netbox/templates/extras/tableconfig_edit.html +++ b/netbox/templates/extras/tableconfig_edit.html @@ -36,10 +36,10 @@
{{ form.columns }} - + {% trans "Move Up" %} - + {% trans "Move Down" %}
diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index d8773feb4..65fcf40af 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -15,7 +15,7 @@ from users.models import * from utilities.data import flatten_dict from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField from utilities.forms.rendering import FieldSet -from utilities.forms.widgets import DateTimePicker +from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget from utilities.permissions import qs_filter_from_constraints __all__ = ( @@ -276,8 +276,13 @@ class ObjectPermissionForm(forms.ModelForm): object_types = ContentTypeMultipleChoiceField( label=_('Object types'), queryset=ObjectType.objects.all(), - limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES, - widget=forms.SelectMultiple(attrs={'size': 6}) + widget=SplitMultiSelectWidget( + choices=[ + (ot.pk, str(ot)) + for ot in ObjectType.objects.filter(OBJECTPERMISSION_OBJECT_TYPES).order_by('app_label', 'model') + ] + ), + help_text=_('Select the types of objects to which the permission will appy.') ) can_view = forms.BooleanField( required=False diff --git a/netbox/utilities/forms/widgets/select.py b/netbox/utilities/forms/widgets/select.py index 8115e2449..7f4e9c87f 100644 --- a/netbox/utilities/forms/widgets/select.py +++ b/netbox/utilities/forms/widgets/select.py @@ -8,6 +8,7 @@ __all__ = ( 'ColorSelect', 'HTMXSelect', 'SelectWithPK', + 'SplitMultiSelectWidget', ) @@ -63,3 +64,79 @@ class SelectWithPK(forms.Select): Include the primary key of each option in the option label (e.g. "Router7 (4721)"). """ option_template_name = 'widgets/select_option_with_pk.html' + + +class AvailableOptions(forms.SelectMultiple): + """ + Renders a including only choices that have _not_ been selected. (For unbound fields, this + will include _all_ choices.) Employed by SplitMultiSelectWidget. + """ + def optgroups(self, name, value, attrs=None): + self.choices = [ + choice for choice in self.choices if str(choice[0]) in value + ] + value = [] # Clear selected choices + return super().optgroups(name, value, attrs) + + +class SplitMultiSelectWidget(forms.MultiWidget): + """ + Renders two