mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 17:26: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 (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
|
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
|
||||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK,
|
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK,
|
||||||
SlugField, SelectSpeedWidget, APISelectWithSelector
|
SlugField, SelectSpeedWidget
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||||
@ -441,27 +441,9 @@ class PlatformForm(NetBoxModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceForm(TenancyForm, 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(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
query_params={
|
with_selector=True
|
||||||
'region_id': '$region',
|
|
||||||
'group_id': '$site_group',
|
|
||||||
},
|
|
||||||
widget=APISelectWithSelector
|
|
||||||
)
|
)
|
||||||
location = DynamicModelChoiceField(
|
location = DynamicModelChoiceField(
|
||||||
queryset=Location.objects.all(),
|
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(
|
device_type = DynamicModelChoiceField(
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
query_params={
|
with_selector=True
|
||||||
'manufacturer_id': '$manufacturer'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
device_role = DynamicModelChoiceField(
|
device_role = DynamicModelChoiceField(
|
||||||
queryset=DeviceRole.objects.all()
|
queryset=DeviceRole.objects.all()
|
||||||
)
|
)
|
||||||
platform = DynamicModelChoiceField(
|
platform = DynamicModelChoiceField(
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
required=False,
|
required=False
|
||||||
query_params={
|
|
||||||
'manufacturer_id': ['$manufacturer', 'null']
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cluster_group = DynamicModelChoiceField(
|
|
||||||
queryset=ClusterGroup.objects.all(),
|
|
||||||
required=False,
|
|
||||||
null_option='None',
|
|
||||||
initial_params={
|
|
||||||
'clusters': '$cluster'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
cluster = DynamicModelChoiceField(
|
cluster = DynamicModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
query_params={
|
with_selector=True
|
||||||
'group_id': '$cluster_group'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
local_context_data = JSONField(
|
local_context_data = JSONField(
|
||||||
@ -537,7 +497,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
virtual_chassis = DynamicModelChoiceField(
|
virtual_chassis = DynamicModelChoiceField(
|
||||||
queryset=VirtualChassis.objects.all(),
|
queryset=VirtualChassis.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
with_selector=True
|
||||||
)
|
)
|
||||||
vc_position = forms.IntegerField(
|
vc_position = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -557,10 +518,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack',
|
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
|
||||||
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
|
'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', 'tenant_group', 'tenant',
|
||||||
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
|
'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'tags',
|
||||||
'description', 'config_template', 'comments', 'tags', 'local_context_data'
|
'local_context_data'
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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.shortcuts import render
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from dcim.filtersets import SiteFilterSet
|
|
||||||
from dcim.forms import SiteFilterForm
|
|
||||||
from dcim.models import Site
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectSelectorView(View):
|
class ObjectSelectorView(View):
|
||||||
template_name = 'htmx/object_selector.html'
|
template_name = 'htmx/object_selector.html'
|
||||||
|
|
||||||
def get(self, request):
|
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)
|
form = form_class(request.GET)
|
||||||
|
|
||||||
if '_search' in request.GET:
|
if '_search' in request.GET:
|
||||||
# Return only search results
|
# Return only search results
|
||||||
model = self._get_model()
|
filterset = self._get_filterset_class(model)
|
||||||
filterset = self._get_filterset_class()
|
|
||||||
|
|
||||||
queryset = model.objects.restrict(request.user)
|
queryset = model.objects.restrict(request.user)
|
||||||
if filterset:
|
if filterset:
|
||||||
@ -28,16 +29,27 @@ class ObjectSelectorView(View):
|
|||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
|
'model': model,
|
||||||
})
|
})
|
||||||
|
|
||||||
def _get_model(self):
|
def _get_model(self, label):
|
||||||
# TODO: Determine model from request parameters
|
try:
|
||||||
return Site
|
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):
|
def _get_form_class(self, model):
|
||||||
# TODO: Determine form class from model
|
if hasattr(self, 'form_class'):
|
||||||
return SiteFilterForm
|
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):
|
def _get_filterset_class(self, model):
|
||||||
# TODO: Determine filterset class from model
|
if hasattr(self, 'filterset_class'):
|
||||||
return SiteFilterSet
|
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">
|
<div class="row mb-2">
|
||||||
<h5 class="offset-sm-3">Hardware</h5>
|
<h5 class="offset-sm-3">Hardware</h5>
|
||||||
</div>
|
</div>
|
||||||
{% render_field form.manufacturer %}
|
|
||||||
{% render_field form.device_type %}
|
{% render_field form.device_type %}
|
||||||
{% render_field form.airflow %}
|
{% render_field form.airflow %}
|
||||||
{% render_field form.serial %}
|
{% render_field form.serial %}
|
||||||
@ -29,8 +28,6 @@
|
|||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<h5 class="offset-sm-3">Location</h5>
|
<h5 class="offset-sm-3">Location</h5>
|
||||||
</div>
|
</div>
|
||||||
{# {% render_field form.region %}#}
|
|
||||||
{# {% render_field form.site_group %}#}
|
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
{% render_field form.location %}
|
{% render_field form.location %}
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
@ -76,7 +73,6 @@
|
|||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<h5 class="offset-sm-3">Virtualization</h5>
|
<h5 class="offset-sm-3">Virtualization</h5>
|
||||||
</div>
|
</div>
|
||||||
{% render_field form.cluster_group %}
|
|
||||||
{% render_field form.cluster %}
|
{% render_field form.cluster %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
<div class="modal-header">
|
<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>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body row">
|
<div class="modal-body row">
|
||||||
@ -9,16 +9,16 @@
|
|||||||
<ul class="nav nav-pills flex-column">
|
<ul class="nav nav-pills flex-column">
|
||||||
{% for field in form.visible_fields %}
|
{% for field in form.visible_fields %}
|
||||||
<li class="nav-item">
|
<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>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9">
|
<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">
|
<div class="tab-content p-1">
|
||||||
{% for field in form.visible_fields %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for object in results %}
|
{% 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">
|
<h6 class="mb-1">
|
||||||
{{ object }}
|
{{ object }}
|
||||||
{% if object.status %}{% badge object.get_status_display bg_color=object.get_status_color %}{% endif %}
|
{% 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
|
filter = django_filters.ModelChoiceFilter
|
||||||
widget = widgets.APISelect
|
widget = widgets.APISelect
|
||||||
|
|
||||||
def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None,
|
def __init__(
|
||||||
fetch_trigger=None, empty_label=None, *args, **kwargs):
|
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.query_params = query_params or {}
|
||||||
self.initial_params = initial_params or {}
|
self.initial_params = initial_params or {}
|
||||||
self.null_option = null_option
|
self.null_option = null_option
|
||||||
self.disabled_indicator = disabled_indicator
|
self.disabled_indicator = disabled_indicator
|
||||||
self.fetch_trigger = fetch_trigger
|
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
|
# to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
|
||||||
# by widget_attrs()
|
# by widget_attrs()
|
||||||
self.to_field_name = kwargs.get('to_field_name')
|
self.to_field_name = kwargs.get('to_field_name')
|
||||||
self.empty_option = empty_label or ""
|
self.empty_option = empty_label or ""
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(queryset, **kwargs)
|
||||||
|
|
||||||
def widget_attrs(self, widget):
|
def widget_attrs(self, widget):
|
||||||
attrs = {
|
attrs = {
|
||||||
@ -70,6 +83,10 @@ class DynamicModelChoiceMixin:
|
|||||||
if (len(self.query_params) > 0):
|
if (len(self.query_params) > 0):
|
||||||
widget.add_query_params(self.query_params)
|
widget.add_query_params(self.query_params)
|
||||||
|
|
||||||
|
# Include object selector?
|
||||||
|
if self.with_selector:
|
||||||
|
attrs['selector'] = self.model._meta.label_lower
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def get_bound_field(self, form, field_name):
|
def get_bound_field(self, form, field_name):
|
||||||
|
@ -11,7 +11,6 @@ from .utils import add_blank_choice, parse_numeric_range
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'APISelect',
|
'APISelect',
|
||||||
'APISelectMultiple',
|
'APISelectMultiple',
|
||||||
'APISelectWithSelector',
|
|
||||||
'BulkEditNullBooleanSelect',
|
'BulkEditNullBooleanSelect',
|
||||||
'ClearableFileInput',
|
'ClearableFileInput',
|
||||||
'ColorSelect',
|
'ColorSelect',
|
||||||
@ -117,6 +116,7 @@ class APISelect(forms.Select):
|
|||||||
|
|
||||||
:param api_url: API endpoint URL. Required if not set automatically by the parent field.
|
: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'
|
option_template_name = 'widgets/select_option.html'
|
||||||
dynamic_params: Dict[str, str]
|
dynamic_params: Dict[str, str]
|
||||||
static_params: Dict[str, List[str]]
|
static_params: Dict[str, List[str]]
|
||||||
@ -260,10 +260,6 @@ class APISelectMultiple(APISelect, forms.SelectMultiple):
|
|||||||
self.attrs['data-multiple'] = 1
|
self.attrs['data-multiple'] = 1
|
||||||
|
|
||||||
|
|
||||||
class APISelectWithSelector(APISelect):
|
|
||||||
template_name = 'widgets/apiselect_with_selector.html'
|
|
||||||
|
|
||||||
|
|
||||||
class DatePicker(forms.TextInput):
|
class DatePicker(forms.TextInput):
|
||||||
"""
|
"""
|
||||||
Date picker using Flatpickr.
|
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