mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
WIP
This commit is contained in:
parent
48cb2036e9
commit
f83352f2c5
@ -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):
|
||||
|
@ -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}')
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
18
netbox/utilities/templates/widgets/apiselect.html
Normal file
18
netbox/utilities/templates/widgets/apiselect.html
Normal 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 %}
|
@ -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>
|
Loading…
Reference in New Issue
Block a user