From b4f15092dbb849ee85fd2dd8caf988ea4bd7eadb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Nov 2024 14:44:57 -0500 Subject: [PATCH] Closes #5858: Implement a quick-add UI widget for related objects (#18016) * WIP * Misc cleanup * Add warning re: nested quick-adds --- netbox/circuits/forms/model_forms.py | 14 +++-- netbox/dcim/forms/model_forms.py | 21 +++++--- netbox/ipam/forms/model_forms.py | 14 +++-- netbox/netbox/views/generic/object_views.py | 41 +++++++++++---- netbox/project-static/dist/netbox.js | Bin 390388 -> 390918 bytes netbox/project-static/dist/netbox.js.map | Bin 527142 -> 527957 bytes netbox/project-static/src/buttons/reslug.ts | 48 +++++++++--------- netbox/project-static/src/htmx.ts | 11 ++-- netbox/project-static/src/quickAdd.ts | 39 ++++++++++++++ netbox/templates/htmx/quick_add.html | 28 ++++++++++ netbox/templates/htmx/quick_add_created.html | 22 ++++++++ netbox/tenancy/forms/forms.py | 1 + netbox/utilities/forms/fields/dynamic.py | 12 ++++- .../templates/widgets/apiselect.html | 27 +++++++--- netbox/virtualization/forms/model_forms.py | 6 ++- netbox/vpn/forms/model_forms.py | 9 ++-- netbox/wireless/forms/model_forms.py | 3 +- 17 files changed, 228 insertions(+), 68 deletions(-) create mode 100644 netbox/project-static/src/quickAdd.ts create mode 100644 netbox/templates/htmx/quick_add.html create mode 100644 netbox/templates/htmx/quick_add_created.html diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 10cd06563..9eeb0f588 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -50,7 +50,9 @@ class ProviderForm(NetBoxModelForm): class ProviderAccountForm(NetBoxModelForm): provider = DynamicModelChoiceField( label=_('Provider'), - queryset=Provider.objects.all() + queryset=Provider.objects.all(), + selector=True, + quick_add=True ) comments = CommentField() @@ -64,7 +66,9 @@ class ProviderAccountForm(NetBoxModelForm): class ProviderNetworkForm(NetBoxModelForm): provider = DynamicModelChoiceField( label=_('Provider'), - queryset=Provider.objects.all() + queryset=Provider.objects.all(), + selector=True, + quick_add=True ) comments = CommentField() @@ -97,7 +101,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm): provider = DynamicModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), - selector=True + selector=True, + quick_add=True ) provider_account = DynamicModelChoiceField( label=_('Provider account'), @@ -108,7 +113,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm): } ) type = DynamicModelChoiceField( - queryset=CircuitType.objects.all() + queryset=CircuitType.objects.all(), + quick_add=True ) comments = CommentField() diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 2fcdbe5fd..b004798af 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -112,12 +112,14 @@ class SiteForm(TenancyForm, NetBoxModelForm): region = DynamicModelChoiceField( label=_('Region'), queryset=Region.objects.all(), - required=False + required=False, + quick_add=True ) group = DynamicModelChoiceField( label=_('Group'), queryset=SiteGroup.objects.all(), - required=False + required=False, + quick_add=True ) asns = DynamicModelMultipleChoiceField( queryset=ASN.objects.all(), @@ -206,7 +208,8 @@ class RackRoleForm(NetBoxModelForm): class RackTypeForm(NetBoxModelForm): manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), - queryset=Manufacturer.objects.all() + queryset=Manufacturer.objects.all(), + quick_add=True ) comments = CommentField() slug = SlugField( @@ -348,7 +351,8 @@ class ManufacturerForm(NetBoxModelForm): class DeviceTypeForm(NetBoxModelForm): manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), - queryset=Manufacturer.objects.all() + queryset=Manufacturer.objects.all(), + quick_add=True ) default_platform = DynamicModelChoiceField( label=_('Default platform'), @@ -436,7 +440,8 @@ class PlatformForm(NetBoxModelForm): manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), - required=False + required=False, + quick_add=True ) config_template = DynamicModelChoiceField( label=_('Config template'), @@ -508,7 +513,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm): ) role = DynamicModelChoiceField( label=_('Device role'), - queryset=DeviceRole.objects.all() + queryset=DeviceRole.objects.all(), + quick_add=True ) platform = DynamicModelChoiceField( label=_('Platform'), @@ -750,7 +756,8 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm): power_panel = DynamicModelChoiceField( label=_('Power panel'), queryset=PowerPanel.objects.all(), - selector=True + selector=True, + quick_add=True ) rack = DynamicModelChoiceField( label=_('Rack'), diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 56a6dc3d9..53ffe8f3f 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -109,7 +109,8 @@ class RIRForm(NetBoxModelForm): class AggregateForm(TenancyForm, NetBoxModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), - label=_('RIR') + label=_('RIR'), + quick_add=True ) comments = CommentField() @@ -132,6 +133,7 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), label=_('RIR'), + quick_add=True ) slug = SlugField() fieldsets = ( @@ -150,6 +152,7 @@ class ASNForm(TenancyForm, NetBoxModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), label=_('RIR'), + quick_add=True ) sites = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -216,7 +219,8 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm): role = DynamicModelChoiceField( label=_('Role'), queryset=Role.objects.all(), - required=False + required=False, + quick_add=True ) comments = CommentField() @@ -246,7 +250,8 @@ class IPRangeForm(TenancyForm, NetBoxModelForm): role = DynamicModelChoiceField( label=_('Role'), queryset=Role.objects.all(), - required=False + required=False, + quick_add=True ) comments = CommentField() @@ -639,7 +644,8 @@ class VLANForm(TenancyForm, NetBoxModelForm): role = DynamicModelChoiceField( label=_('Role'), queryset=Role.objects.all(), - required=False + required=False, + quick_add=True ) qinq_svlan = DynamicModelChoiceField( label=_('Q-in-Q SVLAN'), diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 0686e52b7..fb554ca4f 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -233,18 +233,23 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): form = self.form(instance=obj, initial=initial_data) restrict_form_fields(form, request.user) - # If this is an HTMX request, return only the rendered form HTML - if htmx_partial(request): - return render(request, self.htmx_template_name, { - 'model': model, - 'object': obj, - 'form': form, - }) - - return render(request, self.template_name, { + context = { 'model': model, 'object': obj, 'form': form, + } + + # If the form is being displayed within a "quick add" widget, + # use the appropriate template + if request.GET.get('_quickadd'): + return render(request, 'htmx/quick_add.html', context) + + # If this is an HTMX request, return only the rendered form HTML + if htmx_partial(request): + return render(request, self.htmx_template_name, context) + + return render(request, self.template_name, { + **context, 'return_url': self.get_return_url(request, obj), 'prerequisite_model': get_prerequisite_model(self.queryset), **self.get_extra_context(request, obj), @@ -259,6 +264,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ logger = logging.getLogger('netbox.views.ObjectEditView') obj = self.get_object(**kwargs) + model = self.queryset.model # Take a snapshot for change logging (if editing an existing object) if obj.pk and hasattr(obj, 'snapshot'): @@ -292,6 +298,12 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): msg = f'{msg} {obj}' messages.success(request, msg) + # Object was created via "quick add" modal + if '_quickadd' in request.POST: + return render(request, 'htmx/quick_add_created.html', { + 'object': obj, + }) + # If adding another object, redirect back to the edit form if '_addanother' in request.POST: redirect_url = request.path @@ -324,12 +336,19 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): else: logger.debug("Form validation failed") - return render(request, self.template_name, { + context = { + 'model': model, 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), **self.get_extra_context(request, obj), - }) + } + + # Form was submitted via a "quick add" widget + if '_quickadd' in request.POST: + return render(request, 'htmx/quick_add.html', context) + + return render(request, self.template_name, context) class ObjectDeleteView(GetReturnURLMixin, BaseObjectView): diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 969d5c73a704424190120ac1a6c96af13085b025..5e24ee6250e4ad778886c374c7ca972faf2e3ce6 100644 GIT binary patch delta 9318 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 diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 0f4ac63d3d6199f281667fc18788c7e456278a22..a05e8edb51d286b535fab2371e7b9ad18b49d4b0 100644 GIT binary patch delta 1286 zcmZWpJ#W)s5LSw*jTrb+DX0ulr3}zlR2@J9!Ek=#q-ij&6@_4glsd7Sx^E9>&(c>DM!yUQGD~Q@^N4T)TA&0no<-27IfhQ_`(1v3JLPHAxZ!y z=d1!%)I=G>Ivq=#v@rmtsDKRhNwJfsin3@Tv#0`8ky0))Pf1dl%di3AUZ!w0##Mz& z6?|iyL2S+C6sHAX88oL&H#!+%p34HA>&6^48W}md`4XXBycvx=i74PS@$sIrlqtj) zhRTe(N(FW$UKlFb%xe5-sNA?*q*#>S_gO?HPRmtf+Gu)s*0l9)Lb)Iwm3f&BzHvGbaV$bal? z^3JO9!J0C=!c+Oly0Ak(WxJEM?7t4he@4Rw@#7hFae!O5pt!9Ad1NJ9=2fF?B*$Qh zXyuB)x2z&b>r=D@=wu|j8ly08FJlxclnN8ecweZL3{5<9032djV&Ma{414(G@@7hl z&2O10sJcL;E@AqTSY?4RCYex1ps`M9TqPLRWOF;bxhSaOR}Lx;olApOwwx4V?W#|w@L2vG)GX?(ktaEW(*0i}eI8x8YZ#3<`c2XLDIHz47ebuza F)E}CVZ*>3w delta 593 zcmYjNO-lk%6h)nKC>NEq5DG+a_dp_|O%ZqAjFZYVO(tTrF@4C8jN+)ZF+nF>w{bQt zBeyMrS@#2?RkZ2{v})b1_ugc>xR=ZQIOp8+ejk4v$DeA+VJ|uAB?B~qG(ui4DZHEq zTJFI>c<5&UG>lY$63PSQ5dk26r2yW=d{mwr82|>NBTHsS85!jODoCSU%t}HN?qZ~a zl1Bg7#WY(kpfW(&PZS}ARiyUW^^BO}XflQ89H7uj+3|@udQD;+8p43DCB&t$3uI#e z8#7r=nQFmlx}m^~-;}a*0*U}dw%d|($bB0Mc=OPo8h|grFDb0SZw%c{IYK;;b&nlk zV~vX`@PDV{8%Ib4ad$Lv1R4w8#992j>u*cI|5BKWD~U-?amhvgi9s9i=Pvh{_)hcO zkftr_M2Sh=QjeIf9Q*5(CQawyIs-SS;M_)2;vM%g7G4jWd4pJZ|2djANxhmt@q}IP z)TB('button#reslug')) { + const form = slugButton.form; + if (form == null) continue; + const slugField = form.querySelector('#id_slug') as HTMLInputElement; + if (slugField == null) continue; + const sourceId = slugField.getAttribute('slug-source'); + const sourceField = form.querySelector(`#id_${sourceId}`) as HTMLInputElement; - if (sourceField === null) { - console.error('Unable to find field for slug field.'); - return; - } + const slugLengthAttr = slugField.getAttribute('maxlength'); + let slugLength = 50; - const slugLengthAttr = slugField.getAttribute('maxlength'); - let slugLength = 50; - - if (slugLengthAttr) { - slugLength = Number(slugLengthAttr); - } - sourceField.addEventListener('blur', () => { - if (!slugField.value) { - slugField.value = slugify(sourceField.value, slugLength); + if (slugLengthAttr) { + slugLength = Number(slugLengthAttr); } - }); - slugButton.addEventListener('click', () => { - slugField.value = slugify(sourceField.value, slugLength); - }); + sourceField.addEventListener('blur', () => { + if (!slugField.value) { + slugField.value = slugify(sourceField.value, slugLength); + } + }); + slugButton.addEventListener('click', () => { + slugField.value = slugify(sourceField.value, slugLength); + }); + } } diff --git a/netbox/project-static/src/htmx.ts b/netbox/project-static/src/htmx.ts index f4092036b..6a772011b 100644 --- a/netbox/project-static/src/htmx.ts +++ b/netbox/project-static/src/htmx.ts @@ -4,11 +4,16 @@ import { initSelects } from './select'; import { initObjectSelector } from './objectSelector'; import { initBootstrap } from './bs'; import { initMessages } from './messages'; +import { initQuickAdd } from './quickAdd'; function initDepedencies(): void { - for (const init of [initButtons, initClipboard, initSelects, initObjectSelector, initBootstrap, initMessages]) { - init(); - } + initButtons(); + initClipboard(); + initSelects(); + initObjectSelector(); + initQuickAdd(); + initBootstrap(); + initMessages(); } /** diff --git a/netbox/project-static/src/quickAdd.ts b/netbox/project-static/src/quickAdd.ts new file mode 100644 index 000000000..e038f5d19 --- /dev/null +++ b/netbox/project-static/src/quickAdd.ts @@ -0,0 +1,39 @@ +import { Modal } from 'bootstrap'; + +function handleQuickAddObject(): void { + const quick_add = document.getElementById('quick-add-object'); + if (quick_add == null) return; + + const object_id = quick_add.getAttribute('data-object-id'); + if (object_id == null) return; + const object_repr = quick_add.getAttribute('data-object-repr'); + if (object_repr == null) return; + + const target_id = quick_add.getAttribute('data-target-id'); + if (target_id == null) return; + const target = document.getElementById(target_id); + if (target == null) return; + + //@ts-expect-error tomselect added on init + target.tomselect.addOption({ + id: object_id, + display: object_repr, + }); + //@ts-expect-error tomselect added on init + target.tomselect.addItem(object_id); + + const modal_element = document.getElementById('htmx-modal'); + if (modal_element) { + const modal = Modal.getInstance(modal_element); + if (modal) { + modal.hide(); + } + } +} + +export function initQuickAdd(): void { + const quick_add_modal = document.getElementById('htmx-modal-content'); + if (quick_add_modal) { + quick_add_modal.addEventListener('htmx:afterSwap', () => handleQuickAddObject()); + } +} diff --git a/netbox/templates/htmx/quick_add.html b/netbox/templates/htmx/quick_add.html new file mode 100644 index 000000000..9473e14a1 --- /dev/null +++ b/netbox/templates/htmx/quick_add.html @@ -0,0 +1,28 @@ +{% load form_helpers %} +{% load helpers %} +{% load i18n %} + + + diff --git a/netbox/templates/htmx/quick_add_created.html b/netbox/templates/htmx/quick_add_created.html new file mode 100644 index 000000000..3b1a24c48 --- /dev/null +++ b/netbox/templates/htmx/quick_add_created.html @@ -0,0 +1,22 @@ +{% load form_helpers %} +{% load helpers %} +{% load i18n %} + + + diff --git a/netbox/tenancy/forms/forms.py b/netbox/tenancy/forms/forms.py index 114253e7a..0edb36348 100644 --- a/netbox/tenancy/forms/forms.py +++ b/netbox/tenancy/forms/forms.py @@ -25,6 +25,7 @@ class TenancyForm(forms.Form): label=_('Tenant'), queryset=Tenant.objects.all(), required=False, + quick_add=True, query_params={ 'group_id': '$tenant_group' } diff --git a/netbox/utilities/forms/fields/dynamic.py b/netbox/utilities/forms/fields/dynamic.py index 6666c0e4d..13d5ffc70 100644 --- a/netbox/utilities/forms/fields/dynamic.py +++ b/netbox/utilities/forms/fields/dynamic.py @@ -2,7 +2,7 @@ import django_filters from django import forms from django.conf import settings from django.forms import BoundField -from django.urls import reverse +from django.urls import reverse, reverse_lazy from utilities.forms import widgets from utilities.views import get_viewname @@ -66,6 +66,8 @@ class DynamicModelChoiceMixin: choice (DEPRECATED: pass `context={'disabled': '$fieldname'}` instead) context: A mapping of