diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index c1a8355db..38c4c2cfd 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -14,7 +14,7 @@ from tenancy.forms import TenancyForm from utilities.forms import ( APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, - SlugField, SelectSpeedWidget, APISelectWithSelector + SlugField, SelectSpeedWidget ) from virtualization.models import Cluster, ClusterGroup from wireless.models import WirelessLAN, WirelessLANGroup @@ -441,27 +441,9 @@ class PlatformForm(NetBoxModelForm): class DeviceForm(TenancyForm, NetBoxModelForm): - # region = DynamicModelChoiceField( - # queryset=Region.objects.all(), - # required=False, - # initial_params={ - # 'sites': '$site' - # } - # ) - # site_group = DynamicModelChoiceField( - # queryset=SiteGroup.objects.all(), - # required=False, - # initial_params={ - # 'sites': '$site' - # } - # ) site = DynamicModelChoiceField( queryset=Site.objects.all(), - query_params={ - 'region_id': '$region', - 'group_id': '$site_group', - }, - widget=APISelectWithSelector + with_selector=True ) location = DynamicModelChoiceField( queryset=Location.objects.all(), @@ -492,43 +474,21 @@ class DeviceForm(TenancyForm, NetBoxModelForm): } ) ) - manufacturer = DynamicModelChoiceField( - queryset=Manufacturer.objects.all(), - required=False, - initial_params={ - 'device_types': '$device_type' - } - ) device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), - query_params={ - 'manufacturer_id': '$manufacturer' - } + with_selector=True ) device_role = DynamicModelChoiceField( queryset=DeviceRole.objects.all() ) platform = DynamicModelChoiceField( queryset=Platform.objects.all(), - required=False, - query_params={ - 'manufacturer_id': ['$manufacturer', 'null'] - } - ) - cluster_group = DynamicModelChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - null_option='None', - initial_params={ - 'clusters': '$cluster' - } + required=False ) cluster = DynamicModelChoiceField( queryset=Cluster.objects.all(), required=False, - query_params={ - 'group_id': '$cluster_group' - } + with_selector=True ) comments = CommentField() local_context_data = JSONField( @@ -537,7 +497,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm): ) virtual_chassis = DynamicModelChoiceField( queryset=VirtualChassis.objects.all(), - required=False + required=False, + with_selector=True ) vc_position = forms.IntegerField( required=False, @@ -557,10 +518,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm): class Meta: model = Device fields = [ - 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', - 'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', - 'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', - 'description', 'config_template', 'comments', 'tags', 'local_context_data' + 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face', + 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', 'tenant_group', 'tenant', + 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'tags', + 'local_context_data' ] def __init__(self, *args, **kwargs): diff --git a/netbox/netbox/views/htmx.py b/netbox/netbox/views/htmx.py index f0f54658c..2c38402ff 100644 --- a/netbox/netbox/views/htmx.py +++ b/netbox/netbox/views/htmx.py @@ -1,22 +1,23 @@ +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist +from django.http import Http404 from django.shortcuts import render +from django.utils.module_loading import import_string from django.views.generic import View -from dcim.filtersets import SiteFilterSet -from dcim.forms import SiteFilterForm -from dcim.models import Site - class ObjectSelectorView(View): template_name = 'htmx/object_selector.html' def get(self, request): - form_class = self._get_form_class() + model = self._get_model(request.GET.get('model', '')) + + form_class = self._get_form_class(model) form = form_class(request.GET) if '_search' in request.GET: # Return only search results - model = self._get_model() - filterset = self._get_filterset_class() + filterset = self._get_filterset_class(model) queryset = model.objects.restrict(request.user) if filterset: @@ -28,16 +29,27 @@ class ObjectSelectorView(View): return render(request, self.template_name, { 'form': form, + 'model': model, }) - def _get_model(self): - # TODO: Determine model from request parameters - return Site + def _get_model(self, label): + try: + app_label, model_name = label.split('.') + content_type = ContentType.objects.get_by_natural_key(app_label, model_name) + except (ValueError, ObjectDoesNotExist): + raise Http404 + return content_type.model_class() - def _get_form_class(self): - # TODO: Determine form class from model - return SiteFilterForm + def _get_form_class(self, model): + if hasattr(self, 'form_class'): + return self.form_class + app_label = model._meta.app_label + class_name = f'{model.__name__}FilterForm' + return import_string(f'{app_label}.forms.{class_name}') - def _get_filterset_class(self): - # TODO: Determine filterset class from model - return SiteFilterSet + def _get_filterset_class(self, model): + if hasattr(self, 'filterset_class'): + return self.filterset_class + app_label = model._meta.app_label + class_name = f'{model.__name__}FilterSet' + return import_string(f'{app_label}.filtersets.{class_name}') diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index ba81dfa13..4475dac9b 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -18,7 +18,6 @@
Hardware
- {% render_field form.manufacturer %} {% render_field form.device_type %} {% render_field form.airflow %} {% render_field form.serial %} @@ -29,8 +28,6 @@
Location
-{# {% render_field form.region %}#} -{# {% render_field form.site_group %}#} {% render_field form.site %} {% render_field form.location %} {% render_field form.rack %} @@ -76,7 +73,6 @@
Virtualization
- {% render_field form.cluster_group %} {% render_field form.cluster %} diff --git a/netbox/templates/htmx/object_selector.html b/netbox/templates/htmx/object_selector.html index b7114bd4d..ef9a788a7 100644 --- a/netbox/templates/htmx/object_selector.html +++ b/netbox/templates/htmx/object_selector.html @@ -1,7 +1,7 @@ {% load form_helpers %}
-
+
{% for field in form.visible_fields %} -
{% render_field field %}
+
{% render_field field %}
{% endfor %}
diff --git a/netbox/templates/htmx/object_selector_results.html b/netbox/templates/htmx/object_selector_results.html index cc4ee4ec8..a8608654c 100644 --- a/netbox/templates/htmx/object_selector_results.html +++ b/netbox/templates/htmx/object_selector_results.html @@ -1,6 +1,6 @@
{% for object in results %} - +
{{ object }} {% if object.status %}{% badge object.get_status_display bg_color=object.get_status_color %}{% endif %} diff --git a/netbox/utilities/forms/fields/dynamic.py b/netbox/utilities/forms/fields/dynamic.py index 68e71610c..440a910e0 100644 --- a/netbox/utilities/forms/fields/dynamic.py +++ b/netbox/utilities/forms/fields/dynamic.py @@ -30,20 +30,33 @@ class DynamicModelChoiceMixin: filter = django_filters.ModelChoiceFilter widget = widgets.APISelect - def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, - fetch_trigger=None, empty_label=None, *args, **kwargs): + def __init__( + self, + queryset, + *, + query_params=None, + initial_params=None, + null_option=None, + disabled_indicator=None, + fetch_trigger=None, + empty_label=None, + with_selector=False, + **kwargs + ): + self.model = queryset.model self.query_params = query_params or {} self.initial_params = initial_params or {} self.null_option = null_option self.disabled_indicator = disabled_indicator self.fetch_trigger = fetch_trigger + self.with_selector = with_selector # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference # by widget_attrs() self.to_field_name = kwargs.get('to_field_name') self.empty_option = empty_label or "" - super().__init__(*args, **kwargs) + super().__init__(queryset, **kwargs) def widget_attrs(self, widget): attrs = { @@ -70,6 +83,10 @@ class DynamicModelChoiceMixin: if (len(self.query_params) > 0): widget.add_query_params(self.query_params) + # Include object selector? + if self.with_selector: + attrs['selector'] = self.model._meta.label_lower + return attrs def get_bound_field(self, form, field_name): diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index a1613ebf7..dc982eca0 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -11,7 +11,6 @@ from .utils import add_blank_choice, parse_numeric_range __all__ = ( 'APISelect', 'APISelectMultiple', - 'APISelectWithSelector', 'BulkEditNullBooleanSelect', 'ClearableFileInput', 'ColorSelect', @@ -117,6 +116,7 @@ class APISelect(forms.Select): :param api_url: API endpoint URL. Required if not set automatically by the parent field. """ + template_name = 'widgets/apiselect.html' option_template_name = 'widgets/select_option.html' dynamic_params: Dict[str, str] static_params: Dict[str, List[str]] @@ -260,10 +260,6 @@ class APISelectMultiple(APISelect, forms.SelectMultiple): self.attrs['data-multiple'] = 1 -class APISelectWithSelector(APISelect): - template_name = 'widgets/apiselect_with_selector.html' - - class DatePicker(forms.TextInput): """ Date picker using Flatpickr. diff --git a/netbox/utilities/templates/widgets/apiselect.html b/netbox/utilities/templates/widgets/apiselect.html new file mode 100644 index 000000000..d5aee2009 --- /dev/null +++ b/netbox/utilities/templates/widgets/apiselect.html @@ -0,0 +1,18 @@ +{% if widget.attrs.selector %} +
+ {% include 'django/forms/widgets/select.html' %} + +
+{% else %} + {% include 'django/forms/widgets/select.html' %} +{% endif %} diff --git a/netbox/utilities/templates/widgets/apiselect_with_selector.html b/netbox/utilities/templates/widgets/apiselect_with_selector.html deleted file mode 100644 index aa9b0f62a..000000000 --- a/netbox/utilities/templates/widgets/apiselect_with_selector.html +++ /dev/null @@ -1,14 +0,0 @@ -
- {% include 'django/forms/widgets/select.html' %} - -