This commit is contained in:
jeremystretch 2023-03-03 15:16:04 -05:00
parent 48cb2036e9
commit f83352f2c5
9 changed files with 83 additions and 97 deletions

View File

@ -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):

View File

@ -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}')

View File

@ -18,7 +18,6 @@
<div class="row mb-2">
<h5 class="offset-sm-3">Hardware</h5>
</div>
{% render_field form.manufacturer %}
{% render_field form.device_type %}
{% render_field form.airflow %}
{% render_field form.serial %}
@ -29,8 +28,6 @@
<div class="row mb-2">
<h5 class="offset-sm-3">Location</h5>
</div>
{# {% 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 @@
<div class="row mb-2">
<h5 class="offset-sm-3">Virtualization</h5>
</div>
{% render_field form.cluster_group %}
{% render_field form.cluster %}
</div>

View File

@ -1,7 +1,7 @@
{% load form_helpers %}
<div class="modal-header">
<h5 class="modal-title">Object Selector</h5>
<h5 class="modal-title">Select {{ model|meta:"verbose_name"|bettertitle }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row">
@ -9,16 +9,16 @@
<ul class="nav nav-pills flex-column">
{% for field in form.visible_fields %}
<li class="nav-item">
<a class="nav-link" href="#" data-bs-toggle="tab" data-bs-target="#selector{{ forloop.counter }}">{{ field.label }}</a>
<a class="nav-link" href="#" data-bs-toggle="collapse" data-bs-target="#selector{{ forloop.counter }}">{{ field.label }}</a>
</li>
{% endfor %}
</ul>
</div>
<div class="col-9">
<form hx-get="{% url 'htmx_object_selector' %}" hx-target="#results_list">
<form hx-get="{% url 'htmx_object_selector' %}?model={{ model|meta:"label_lower" }}" hx-target="#results_list">
<div class="tab-content p-1">
{% for field in form.visible_fields %}
<div class="tab-pane{% if forloop.first %} active{% endif %}" id="selector{{ forloop.counter }}" role="tabpanel">{% render_field field %}</div>
<div class="collapse{% if forloop.first %} show{% endif %}" id="selector{{ forloop.counter }}">{% render_field field %}</div>
{% endfor %}
</div>
<div class="text-end">

View File

@ -1,6 +1,6 @@
<div class="list-group">
{% for object in results %}
<a href="#" class="list-group-item list-group-item-action">
<a href="#" class="list-group-item list-group-item-action" data-label="{{ object }}" data-value="{{ object.pk }}">
<h6 class="mb-1">
{{ object }}
{% if object.status %}{% badge object.get_status_display bg_color=object.get_status_color %}{% endif %}

View File

@ -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):

View File

@ -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.

View File

@ -0,0 +1,18 @@
{% if widget.attrs.selector %}
<div class="d-flex">
{% include 'django/forms/widgets/select.html' %}
<button
type="button"
title="Open selector"
class="btn btn-sm btn-outline-dark border-input ms-1"
data-bs-toggle="modal"
data-bs-target="#htmx-modal"
hx-get="{% url 'htmx_object_selector' %}?model={{ widget.attrs.selector }}"
hx-target="#htmx-modal-content"
>
<i class="mdi mdi-database-search-outline"></i>
</button>
</div>
{% else %}
{% include 'django/forms/widgets/select.html' %}
{% endif %}

View File

@ -1,14 +0,0 @@
<div class="d-flex">
{% include 'django/forms/widgets/select.html' %}
<button
type="button"
title="Open selector"
class="btn btn-sm btn-outline-dark border-input ms-1"
data-bs-toggle="modal"
data-bs-target="#htmx-modal"
hx-get="{% url 'htmx_object_selector' %}"
hx-target="#htmx-modal-content"
>
<i class="mdi mdi-database-search-outline"></i>
</button>
</div>