Merge branch 'develop' into 3122-connection-device-select2

This commit is contained in:
Saria Hajjar
2020-01-02 20:40:19 +00:00
14 changed files with 153 additions and 41 deletions

View File

@@ -978,9 +978,12 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
method='filter_site',
label='Site (slug)',
)
device = django_filters.CharFilter(
device_id = MultiValueNumberFilter(
method='filter_device'
)
device = MultiValueCharFilter(
method='filter_device',
label='Device',
field_name='device__name'
)
class Meta:
@@ -993,11 +996,11 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
return queryset.filter(connected_endpoint__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value.strip():
if not value:
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(connected_endpoint__device__name__icontains=value)
Q(**{'{}__in'.format(name): value}) |
Q(**{'connected_endpoint__{}__in'.format(name): value})
)
@@ -1006,9 +1009,12 @@ class PowerConnectionFilter(django_filters.FilterSet):
method='filter_site',
label='Site (slug)',
)
device = django_filters.CharFilter(
device_id = MultiValueNumberFilter(
method='filter_device'
)
device = MultiValueCharFilter(
method='filter_device',
label='Device',
field_name='device__name'
)
class Meta:
@@ -1021,11 +1027,11 @@ class PowerConnectionFilter(django_filters.FilterSet):
return queryset.filter(_connected_poweroutlet__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value.strip():
if not value:
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(_connected_poweroutlet__device__name__icontains=value)
Q(**{'{}__in'.format(name): value}) |
Q(**{'_connected_poweroutlet__{}__in'.format(name): value})
)
@@ -1034,9 +1040,12 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
method='filter_site',
label='Site (slug)',
)
device = django_filters.CharFilter(
device_id = MultiValueNumberFilter(
method='filter_device'
)
device = MultiValueCharFilter(
method='filter_device',
label='Device',
field_name='device__name'
)
class Meta:
@@ -1052,11 +1061,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
)
def filter_device(self, queryset, name, value):
if not value.strip():
if not value:
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(_connected_interface__device__name__icontains=value)
Q(**{'{}__in'.format(name): value}) |
Q(**{'_connected_interface__{}__in'.format(name): value})
)

View File

@@ -3297,9 +3297,12 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = InventoryItem
fields = [
'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
]
widgets = {
'device': APISelect(
api_url="/api/dcim/devices/"
),
'manufacturer': APISelect(
api_url="/api/dcim/manufacturers/"
)
@@ -3335,9 +3338,19 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
queryset=InventoryItem.objects.all(),
widget=forms.MultipleHiddenInput()
)
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/devices/"
)
)
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
required=False,
widget=APISelect(
api_url="/api/dcim/manufacturers/"
)
)
part_id = forms.CharField(
max_length=50,
@@ -3371,11 +3384,14 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
manufacturer = FilterChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='slug',
null_label='-- None --'
widget=APISelect(
api_url="/api/dcim/manufacturers/",
value_field="slug",
)
)
discovered = forms.NullBooleanField(
required=False,
widget=forms.Select(
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)

View File

@@ -2597,7 +2597,7 @@ class DeviceBay(ComponentModel):
# Check that the installed device is not already installed elsewhere
if self.installed_device:
current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
if current_bay:
if current_bay and current_bay != self:
raise ValidationError({
'installed_device': "Cannot install the specified device; device is already installed in {}".format(
current_bay

View File

@@ -3,7 +3,10 @@ from django.contrib import admin
from netbox.admin import admin_site
from utilities.forms import LaxURLField
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook
from .models import (
CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, TopologyMap, Webhook,
)
from .reports import get_report
def order_content_types(field):
@@ -166,6 +169,36 @@ class ExportTemplateAdmin(admin.ModelAdmin):
form = ExportTemplateForm
#
# Reports
#
@admin.register(ReportResult, site=admin_site)
class ReportResultAdmin(admin.ModelAdmin):
list_display = [
'report', 'active', 'created', 'user', 'passing',
]
fields = [
'report', 'user', 'passing', 'data',
]
list_filter = [
'failed',
]
readonly_fields = fields
def has_add_permission(self, request):
return False
def active(self, obj):
module, report_name = obj.report.split('.')
return True if get_report(module, report_name) else False
active.boolean = True
def passing(self, obj):
return not obj.failed
passing.boolean = True
#
# Topology maps
#

View File

@@ -52,7 +52,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
else:
initial = None
field = forms.NullBooleanField(
required=cf.required, initial=initial, widget=forms.Select(choices=choices)
required=cf.required, initial=initial, widget=StaticSelect2(choices=choices)
)
# Date
@@ -71,7 +71,9 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
default_choice = cf.choices.get(value=initial).pk
except ObjectDoesNotExist:
pass
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
field = forms.TypedChoiceField(
choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2()
)
# URL
elif cf.type == CF_TYPE_URL:

View File

@@ -915,6 +915,13 @@ class ReportResult(models.Model):
class Meta:
ordering = ['report']
def __str__(self):
return "{} {} at {}".format(
self.report,
"passed" if not self.failed else "failed",
self.created
)
#
# Change logging

View File

@@ -46,12 +46,17 @@ def custom_links(obj):
# Add non-grouped links
else:
text_rendered = render_jinja2(cl.text, context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
template_code += LINK_BUTTON.format(
cl.url, link_target, cl.button_class, text_rendered
)
try:
text_rendered = render_jinja2(cl.text, context)
if text_rendered:
link_rendered = render_jinja2(cl.url, context)
link_target = ' target="_blank"' if cl.new_window else ''
template_code += LINK_BUTTON.format(
link_rendered, link_target, cl.button_class, text_rendered
)
except Exception as e:
template_code += '<a class="btn btn-sm btn-default" disabled="disabled" title="{}">' \
'<i class="fa fa-warning"></i> {}</a>\n'.format(e, cl.name)
# Add grouped links to template
for group, links in group_names.items():
@@ -59,11 +64,17 @@ def custom_links(obj):
links_rendered = []
for cl in links:
text_rendered = render_jinja2(cl.text, context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
try:
text_rendered = render_jinja2(cl.text, context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
links_rendered.append(
GROUP_LINK.format(cl.url, link_target, cl.text)
)
except Exception as e:
links_rendered.append(
GROUP_LINK.format(cl.url, link_target, cl.text)
'<li><a disabled="disabled" title="{}"><span class="text-muted">'
'<i class="fa fa-warning"></i> {}</span></a></li>'.format(e, cl.name)
)
if links_rendered:
@@ -71,7 +82,4 @@ def custom_links(obj):
links[0].button_class, group, ''.join(links_rendered)
)
# Render template
rendered = render_jinja2(template_code, context)
return mark_safe(rendered)
return mark_safe(template_code)

View File

@@ -309,6 +309,10 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
queryset=Interface.objects.all(),
label='Interface (ID)',
)
assigned_to_interface = django_filters.BooleanFilter(
method='_assigned_to_interface',
label='Is assigned to an interface',
)
status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES,
null_value=None
@@ -366,6 +370,9 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
except Device.DoesNotExist:
return queryset.none()
def _assigned_to_interface(self, queryset, name, value):
return queryset.exclude(interface__isnull=value)
class VLANGroupFilter(NameSlugSearchFilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(

View File

@@ -938,7 +938,8 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = IPAddress
field_order = [
'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant',
'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'assigned_to_interface', 'tenant_group',
'tenant',
]
q = forms.CharField(
required=False,
@@ -984,6 +985,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
required=False,
widget=StaticSelect2Multiple()
)
assigned_to_interface = forms.NullBooleanField(
required=False,
label='Assigned to an interface',
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
#

View File

@@ -103,14 +103,16 @@ $(document).ready(function() {
placeholder: "---------",
theme: "bootstrap",
templateResult: colorPickerClassCopy,
templateSelection: colorPickerClassCopy
templateSelection: colorPickerClassCopy,
width: "off"
});
// Static choice selection
$('.netbox-select2-static').select2({
allowClear: true,
placeholder: "---------",
theme: "bootstrap"
theme: "bootstrap",
width: "off"
});
// API backed selection
@@ -120,6 +122,7 @@ $(document).ready(function() {
allowClear: true,
placeholder: "---------",
theme: "bootstrap",
width: "off",
ajax: {
delay: 500,
@@ -299,7 +302,8 @@ $(document).ready(function() {
multiple: true,
allowClear: true,
placeholder: "Tags",
theme: "bootstrap",
width: "off",
ajax: {
delay: 250,
url: netbox_api_path + "extras/tags/",

View File

@@ -285,6 +285,8 @@ class APISelect(SelectWithDisabled):
name of the query param and the value if the query param's value.
:param null_option: If true, include the static null option in the selection list.
"""
# Only preload the selected option(s); new options are dynamically displayed and added via the API
template_name = 'widgets/select_api.html'
def __init__(
self,

View File

@@ -0,0 +1,5 @@
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<optgroup label="{{ group_name }}">{% endif %}{% for widget in group_choices %}{% if widget.attrs.selected %}
{% include widget.template_name %}{% endif %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %}
</select>