diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 854c5ebed..90a3d6ce0 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ from circuits.models import Circuit, CircuitTermination @@ -82,14 +83,22 @@ def get_cable_form(a_type, b_type): class _CableForm(CableForm, metaclass=FormMetaclass): - def __init__(self, *args, **kwargs): + def __init__(self, *args, initial=None, **kwargs): + + initial = initial or {} + if a_type: + ct = ContentType.objects.get_for_model(a_type) + initial['a_terminations_type'] = f'{ct.app_label}.{ct.model}' + if b_type: + ct = ContentType.objects.get_for_model(b_type) + initial['b_terminations_type'] = f'{ct.app_label}.{ct.model}' # TODO: Temporary hack to work around list handling limitations with utils.normalize_querydict() for field_name in ('a_terminations', 'b_terminations'): - if field_name in kwargs.get('initial', {}) and type(kwargs['initial'][field_name]) is not list: - kwargs['initial'][field_name] = [kwargs['initial'][field_name]] + if field_name in initial and type(initial[field_name]) is not list: + initial[field_name] = [initial[field_name]] - super().__init__(*args, **kwargs) + super().__init__(*args, initial=initial, **kwargs) if self.instance and self.instance.pk: # Initialize A/B terminations when modifying an existing Cable instance @@ -100,7 +109,7 @@ def get_cable_form(a_type, b_type): super().clean() # Set the A/B terminations on the Cable instance - self.instance.a_terminations = self.cleaned_data['a_terminations'] - self.instance.b_terminations = self.cleaned_data['b_terminations'] + self.instance.a_terminations = self.cleaned_data.get('a_terminations', []) + self.instance.b_terminations = self.cleaned_data.get('b_terminations', []) return _CableForm diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index cee8fcfba..cc7100e3c 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -13,8 +13,7 @@ from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms import BootstrapMixin, add_blank_choice from utilities.forms.fields import ( - CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, - NumericArrayField, SlugField, + CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from virtualization.models import Cluster @@ -616,14 +615,33 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm): self.fields['adopt_components'].disabled = True +def get_termination_type_choices(): + return add_blank_choice([ + (f'{ct.app_label}.{ct.model}', ct.model_class()._meta.verbose_name.title()) + for ct in ContentType.objects.filter(CABLE_TERMINATION_MODELS) + ]) + + class CableForm(TenancyForm, NetBoxModelForm): + a_terminations_type = forms.ChoiceField( + choices=get_termination_type_choices, + required=False, + widget=HTMXSelect(), + label=_('Type') + ) + b_terminations_type = forms.ChoiceField( + choices=get_termination_type_choices, + required=False, + widget=HTMXSelect(), + label=_('Type') + ) comments = CommentField() class Meta: model = Cable fields = [ - 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', - 'comments', 'tags', + 'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', + 'length', 'length_unit', 'description', 'comments', 'tags', ] error_messages = { 'length': { diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ce4bb5750..805e9d881 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -3183,34 +3183,29 @@ class CableView(generic.ObjectView): class CableEditView(generic.ObjectEditView): queryset = Cable.objects.all() template_name = 'dcim/cable_edit.html' + htmx_template_name = 'dcim/htmx/cable_edit.html' - def dispatch(self, request, *args, **kwargs): - - # If creating a new Cable, initialize the form class using URL query params - if 'pk' not in kwargs: - self.form = forms.get_cable_form( - a_type=CABLE_TERMINATION_TYPES.get(request.GET.get('a_terminations_type')), - b_type=CABLE_TERMINATION_TYPES.get(request.GET.get('b_terminations_type')) - ) - - return super().dispatch(request, *args, **kwargs) - - def get_object(self, **kwargs): + def alter_object(self, obj, request, url_args, url_kwargs): """ - Hack into get_object() to set the form class when editing an existing Cable, since ObjectEditView + Hack into alter_object() to set the form class when editing an existing Cable, since ObjectEditView doesn't currently provide a hook for dynamic class resolution. """ - obj = super().get_object(**kwargs) + a_terminations_type = CABLE_TERMINATION_TYPES.get( + request.GET.get('a_terminations_type') or request.POST.get('a_terminations_type') + ) + b_terminations_type = CABLE_TERMINATION_TYPES.get( + request.GET.get('b_terminations_type') or request.POST.get('b_terminations_type') + ) if obj.pk: - # TODO: Optimize this logic - termination_a = obj.terminations.filter(cable_end='A').first() - a_type = termination_a.termination._meta.model if termination_a else None - termination_b = obj.terminations.filter(cable_end='B').first() - b_type = termination_b.termination._meta.model if termination_b else None - self.form = forms.get_cable_form(a_type, b_type) + if not a_terminations_type and (termination_a := obj.terminations.filter(cable_end='A').first()): + a_terminations_type = termination_a.termination._meta.model + if not b_terminations_type and (termination_b := obj.terminations.filter(cable_end='B').first()): + b_terminations_type = termination_b.termination._meta.model - return obj + self.form = forms.get_cable_form(a_terminations_type, b_terminations_type) + + return super().alter_object(obj, request, url_args, url_kwargs) def get_extra_addanother_params(self, request): diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 031b23b8f..57ea45274 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -167,6 +167,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ template_name = 'generic/object_edit.html' form = None + htmx_template_name = 'htmx/form.html' def dispatch(self, request, *args, **kwargs): # Determine required permission based on whether we are editing an existing object @@ -228,7 +229,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): # If this is an HTMX request, return only the rendered form HTML if is_htmx(request): - return render(request, 'htmx/form.html', { + return render(request, self.htmx_template_name, { 'form': form, }) diff --git a/netbox/templates/dcim/cable_edit.html b/netbox/templates/dcim/cable_edit.html index 14d763dc3..fbe877d87 100644 --- a/netbox/templates/dcim/cable_edit.html +++ b/netbox/templates/dcim/cable_edit.html @@ -1,90 +1,5 @@ {% extends 'generic/object_edit.html' %} -{% load static %} -{% load helpers %} -{% load form_helpers %} -{% load i18n %} {% block form %} - - {# A side termination #} -