Merge pull request #8257 from netbox-community/develop

Release v3.1.5
This commit is contained in:
Jeremy Stretch 2022-01-06 09:52:54 -05:00 committed by GitHub
commit d3e2241ff7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 470 additions and 311 deletions

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.1.4
placeholder: v3.1.5
validations:
required: true
- type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.1.4
placeholder: v3.1.5
validations:
required: true
- type: dropdown

View File

@ -1,5 +1,23 @@
# NetBox v3.1
## v3.1.5 (2022-01-06)
### Enhancements
* [#8231](https://github.com/netbox-community/netbox/issues/8231) - Use in-page dialogs for confirming object deletion
* [#8244](https://github.com/netbox-community/netbox/issues/8244) - Add length & length unit fields to cable filter form
* [#8252](https://github.com/netbox-community/netbox/issues/8252) - Linkify type and group columns in clusters table
### Bug Fixes
* [#8213](https://github.com/netbox-community/netbox/issues/8213) - Fix ValueError exception under prefix IP addresses view
* [#8224](https://github.com/netbox-community/netbox/issues/8224) - Fix KeyError exception when creating FHRP group with IP address and protocol "other"
* [#8226](https://github.com/netbox-community/netbox/issues/8226) - Honor return URL after populating a device bay
* [#8228](https://github.com/netbox-community/netbox/issues/8228) - Optional ChoiceVar fields should not force a selection
* [#8255](https://github.com/netbox-community/netbox/issues/8255) - Fix bulk editing of authentication parameters for wireless LANs and links
---
## v3.1.4 (2022-01-03)
### Enhancements

View File

@ -578,7 +578,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
field_groups = [
['q', 'tag'],
['site_id', 'rack_id', 'device_id'],
['type', 'status', 'color'],
['type', 'status', 'color', 'length', 'length_unit'],
['tenant_group_id', 'tenant_id'],
]
region_id = DynamicModelMultipleChoiceField(
@ -603,6 +603,16 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'site_id': '$site_id'
}
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
'tenant_id': '$tenant_id',
'rack_id': '$rack_id',
},
label=_('Device')
)
type = forms.MultipleChoiceField(
choices=add_blank_choice(CableTypeChoices),
required=False,
@ -616,15 +626,12 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
color = ColorField(
required=False
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
'tenant_id': '$tenant_id',
'rack_id': '$rack_id',
},
label=_('Device')
length = forms.IntegerField(
required=False
)
length_unit = forms.ChoiceField(
choices=add_blank_choice(CableLengthUnitChoices),
required=False
)
tag = TagFilterField(model)

View File

@ -2035,8 +2035,9 @@ class DeviceBayPopulateView(generic.ObjectEditView):
device_bay.installed_device = form.cleaned_data['installed_device']
device_bay.save()
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
return_url = self.get_return_url(request)
return redirect('dcim:device', pk=device_bay.device.pk)
return redirect(return_url)
return render(request, 'dcim/devicebay_populate.html', {
'device_bay': device_bay,

View File

@ -7,8 +7,8 @@ from extras.models import *
from extras.utils import FeatureQuery
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField,
ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
)
from virtualization.models import Cluster, ClusterGroup
@ -41,6 +41,10 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
('Values', ('default', 'choices')),
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
)
widgets = {
'type': StaticSelect(),
'filter_logic': StaticSelect(),
}
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
@ -57,6 +61,7 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
('Templates', ('link_text', 'link_url')),
)
widgets = {
'button_class': StaticSelect(),
'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
}
@ -96,8 +101,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
model = Webhook
fields = '__all__'
fieldsets = (
('Webhook', ('name', 'enabled')),
('Assigned Models', ('content_types',)),
('Webhook', ('name', 'content_types', 'enabled')),
('Events', ('type_create', 'type_update', 'type_delete')),
('HTTP Request', (
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
@ -105,7 +109,13 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
('Conditions', ('conditions',)),
('SSL', ('ssl_verification', 'ca_file_path')),
)
labels = {
'type_create': 'Creations',
'type_update': 'Updates',
'type_delete': 'Deletions',
}
widgets = {
'http_method': StaticSelect(),
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
}

View File

@ -21,7 +21,7 @@ from extras.models import JobResult
from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
from utilities.exceptions import AbortTransaction
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from .context_managers import change_logging
from .forms import ScriptForm
@ -164,16 +164,22 @@ class ChoiceVar(ScriptVariable):
def __init__(self, choices, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set field choices
self.field_attrs['choices'] = choices
# Set field choices, adding a blank choice to avoid forced selections
self.field_attrs['choices'] = add_blank_choice(choices)
class MultiChoiceVar(ChoiceVar):
class MultiChoiceVar(ScriptVariable):
"""
Like ChoiceVar, but allows for the selection of multiple choices.
"""
form_field = forms.MultipleChoiceField
def __init__(self, choices, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set field choices
self.field_attrs['choices'] = choices
class ObjectVar(ScriptVariable):
"""

View File

@ -65,6 +65,7 @@ FHRP_PROTOCOL_ROLE_MAPPINGS = {
FHRPGroupProtocolChoices.PROTOCOL_HSRP: IPAddressRoleChoices.ROLE_HSRP,
FHRPGroupProtocolChoices.PROTOCOL_GLBP: IPAddressRoleChoices.ROLE_GLBP,
FHRPGroupProtocolChoices.PROTOCOL_CARP: IPAddressRoleChoices.ROLE_CARP,
FHRPGroupProtocolChoices.PROTOCOL_OTHER: IPAddressRoleChoices.ROLE_VIP,
}

View File

@ -580,7 +580,7 @@ class FHRPGroupForm(CustomFieldModelForm):
vrf=self.cleaned_data['ip_vrf'],
address=self.cleaned_data['ip_address'],
status=self.cleaned_data['ip_status'],
role=FHRP_PROTOCOL_ROLE_MAPPINGS[self.cleaned_data['protocol']],
role=FHRP_PROTOCOL_ROLE_MAPPINGS.get(self.cleaned_data['protocol'], IPAddressRoleChoices.ROLE_VIP),
assigned_object=instance
)
ipaddress.save()
@ -628,8 +628,7 @@ class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
class VLANGroupForm(CustomFieldModelForm):
scope_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
required=False,
widget=StaticSelect
required=False
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),

View File

@ -1,5 +1,7 @@
import datetime
from django.test import override_settings
from django.urls import reverse
from netaddr import IPNetwork
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
@ -222,6 +224,21 @@ class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'description': 'New description',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_aggregate_prefixes(self):
rir = RIR.objects.first()
aggregate = Aggregate.objects.create(prefix=IPNetwork('192.168.0.0/16'), rir=rir)
prefixes = (
Prefix(prefix=IPNetwork('192.168.1.0/24')),
Prefix(prefix=IPNetwork('192.168.2.0/24')),
Prefix(prefix=IPNetwork('192.168.3.0/24')),
)
Prefix.objects.bulk_create(prefixes)
self.assertEqual(aggregate.get_child_prefixes().count(), 3)
url = reverse('ipam:aggregate_prefixes', kwargs={'pk': aggregate.pk})
self.assertHttpStatus(self.client.get(url), 200)
class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = Role
@ -319,6 +336,48 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'description': 'New description',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_prefixes(self):
prefixes = (
Prefix(prefix=IPNetwork('192.168.0.0/16')),
Prefix(prefix=IPNetwork('192.168.1.0/24')),
Prefix(prefix=IPNetwork('192.168.2.0/24')),
Prefix(prefix=IPNetwork('192.168.3.0/24')),
)
Prefix.objects.bulk_create(prefixes)
self.assertEqual(prefixes[0].get_child_prefixes().count(), 3)
url = reverse('ipam:prefix_prefixes', kwargs={'pk': prefixes[0].pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_ipranges(self):
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
ip_ranges = (
IPRange(start_address='192.168.0.1/24', end_address='192.168.0.100/24', size=99),
IPRange(start_address='192.168.1.1/24', end_address='192.168.1.100/24', size=99),
IPRange(start_address='192.168.2.1/24', end_address='192.168.2.100/24', size=99),
)
IPRange.objects.bulk_create(ip_ranges)
self.assertEqual(prefix.get_child_ranges().count(), 3)
url = reverse('ipam:prefix_ipranges', kwargs={'pk': prefix.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_ipaddresses(self):
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
ip_addresses = (
IPAddress(address=IPNetwork('192.168.0.1/16')),
IPAddress(address=IPNetwork('192.168.0.2/16')),
IPAddress(address=IPNetwork('192.168.0.3/16')),
)
IPAddress.objects.bulk_create(ip_addresses)
self.assertEqual(prefix.get_child_ips().count(), 3)
url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk})
self.assertHttpStatus(self.client.get(url), 200)
class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IPRange
@ -377,6 +436,24 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'description': 'New description',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_iprange_ipaddresses(self):
iprange = IPRange.objects.create(
start_address=IPNetwork('192.168.0.1/24'),
end_address=IPNetwork('192.168.0.100/24'),
size=99
)
ip_addresses = (
IPAddress(address=IPNetwork('192.168.0.1/24')),
IPAddress(address=IPNetwork('192.168.0.2/24')),
IPAddress(address=IPNetwork('192.168.0.3/24')),
)
IPAddress.objects.bulk_create(ip_addresses)
self.assertEqual(iprange.get_child_ips().count(), 3)
url = reverse('ipam:iprange_ipaddresses', kwargs={'pk': iprange.pk})
self.assertHttpStatus(self.client.get(url), 200)
class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IPAddress

View File

@ -505,9 +505,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
template_name = 'ipam/prefix/ip_addresses.html'
def get_children(self, request, parent):
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related(
'vrf', 'role', 'tenant',
)
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant')
def prep_table_data(self, request, queryset, parent):
show_available = bool(request.GET.get('show_available', 'true') == 'true')
@ -531,7 +529,6 @@ class PrefixEditView(generic.ObjectEditView):
class PrefixDeleteView(generic.ObjectDeleteView):
queryset = Prefix.objects.all()
template_name = 'ipam/prefix_delete.html'
class PrefixBulkImportView(generic.BulkImportView):

View File

@ -19,7 +19,7 @@ from netbox.config import PARAMS
# Environment setup
#
VERSION = '3.1.4'
VERSION = '3.1.5'
# Hostname
HOSTNAME = platform.node()

View File

@ -10,6 +10,7 @@ from django.db.models import ManyToManyField, ProtectedError
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import escape
from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
@ -430,10 +431,21 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
obj = self.get_object(kwargs)
form = ConfirmationForm(initial=request.GET)
return render(request, self.template_name, {
'obj': obj,
# If this is an HTMX request, return only the rendered deletion form as modal content
if is_htmx(request):
viewname = f'{self.queryset.model._meta.app_label}:{self.queryset.model._meta.model_name}_delete'
form_url = reverse(viewname, kwargs={'pk': obj.pk})
return render(request, 'htmx/delete_form.html', {
'object': obj,
'object_type': self.queryset.model._meta.verbose_name,
'form': form,
'form_url': form_url,
})
return render(request, self.template_name, {
'object': obj,
'object_type': self.queryset.model._meta.verbose_name,
'form': form,
'obj_type': self.queryset.model._meta.verbose_name,
'return_url': self.get_return_url(request, obj),
})
@ -466,9 +478,9 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
logger.debug("Form validation failed")
return render(request, self.template_name, {
'obj': obj,
'object': obj,
'object_type': self.queryset.model._meta.verbose_name,
'form': form,
'obj_type': self.queryset.model._meta.verbose_name,
'return_url': self.get_return_url(request, obj),
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -358,7 +358,7 @@ nav.search {
// Don't overtake dropdowns
z-index: 999;
justify-content: center;
background-color: var(--nbx-body-bg);
background-color: $navbar-light-color;
.search-container {
display: flex;
@ -452,8 +452,8 @@ main.login-container {
}
.footer {
background-color: $tab-content-bg;
padding: 0;
.nav-link {
padding: 0.5rem;
}
@ -517,6 +517,10 @@ h6.accordion-item-title {
}
}
.navbar {
border-bottom: 1px solid $border-color;
}
.navbar-brand {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
@ -554,6 +558,7 @@ div.content-container {
}
div.content {
background-color: $tab-content-bg;
flex: 1;
}
@ -898,6 +903,7 @@ div.card-overlay {
// Tabbed content
.nav-tabs {
background-color: $body-bg;
.nav-link {
&:hover {
// Don't show a bottom-border on a hovered nav link because it overlaps with the .nav-tab border.
@ -919,14 +925,6 @@ div.card-overlay {
display: flex;
flex-direction: column;
padding: $spacer;
background-color: $tab-content-bg;
border-bottom: 1px solid $nav-tabs-border-color;
// Remove background and border when printing.
@media print {
background-color: var(--nbx-body-bg) !important;
border-bottom: none !important;
}
}
// Override masonry-layout styles when printing.

View File

@ -33,95 +33,6 @@ $darkest: #171b1d;
@import '../node_modules/bootstrap/scss/variables';
// Make color palette colors available as theme colors.
// For example, you could use `.bg-red-100`, if needed.
$theme-color-addons: (
'darker': $darker,
'darkest': $darkest,
'gray': $gray-400,
'gray-100': $gray-100,
'gray-200': $gray-200,
'gray-300': $gray-300,
'gray-400': $gray-400,
'gray-500': $gray-500,
'gray-600': $gray-600,
'gray-700': $gray-700,
'gray-800': $gray-800,
'gray-900': $gray-900,
'red-100': $red-100,
'red-200': $red-200,
'red-300': $red-300,
'red-400': $red-400,
'red-500': $red-500,
'red-600': $red-600,
'red-700': $red-700,
'red-800': $red-800,
'red-900': $red-900,
'yellow-100': $yellow-100,
'yellow-200': $yellow-200,
'yellow-300': $yellow-300,
'yellow-400': $yellow-400,
'yellow-500': $yellow-500,
'yellow-600': $yellow-600,
'yellow-700': $yellow-700,
'yellow-800': $yellow-800,
'yellow-900': $yellow-900,
'green-100': $green-100,
'green-200': $green-200,
'green-300': $green-300,
'green-400': $green-400,
'green-500': $green-500,
'green-600': $green-600,
'green-700': $green-700,
'green-800': $green-800,
'green-900': $green-900,
'blue-100': $blue-100,
'blue-200': $blue-200,
'blue-300': $blue-300,
'blue-400': $blue-400,
'blue-500': $blue-500,
'blue-600': $blue-600,
'blue-700': $blue-700,
'blue-800': $blue-800,
'blue-900': $blue-900,
'cyan-100': $cyan-100,
'cyan-200': $cyan-200,
'cyan-300': $cyan-300,
'cyan-400': $cyan-400,
'cyan-500': $cyan-500,
'cyan-600': $cyan-600,
'cyan-700': $cyan-700,
'cyan-800': $cyan-800,
'cyan-900': $cyan-900,
'indigo-100': $indigo-100,
'indigo-200': $indigo-200,
'indigo-300': $indigo-300,
'indigo-400': $indigo-400,
'indigo-500': $indigo-500,
'indigo-600': $indigo-600,
'indigo-700': $indigo-700,
'indigo-800': $indigo-800,
'indigo-900': $indigo-900,
'purple-100': $purple-100,
'purple-200': $purple-200,
'purple-300': $purple-300,
'purple-400': $purple-400,
'purple-500': $purple-500,
'purple-600': $purple-600,
'purple-700': $purple-700,
'purple-800': $purple-800,
'purple-900': $purple-900,
'pink-100': $pink-100,
'pink-200': $pink-200,
'pink-300': $pink-300,
'pink-400': $pink-400,
'pink-500': $pink-500,
'pink-600': $pink-600,
'pink-700': $pink-700,
'pink-800': $pink-800,
'pink-900': $pink-900,
);
// This is the same value as the default from Bootstrap, but it needs to be in scope prior to
// importing _variables.scss from Bootstrap.
$btn-close-width: 1em;

View File

@ -3,6 +3,7 @@
@use 'sass:map';
@import './theme-base';
// Theme colors (BS5 classes)
$primary: $blue-300;
$secondary: $gray-500;
$success: $green-300;
@ -13,6 +14,7 @@ $light: $gray-300;
$dark: $gray-500;
$theme-colors: (
// BS5 theme colors
'primary': $primary,
'secondary': $secondary,
'success': $success,
@ -21,18 +23,23 @@ $theme-colors: (
'danger': $danger,
'light': $light,
'dark': $dark,
'red': $red-300,
'yellow': $yellow-300,
'green': $green-300,
// General-purpose palette
'blue': $blue-300,
'cyan': $cyan-300,
'indigo': $indigo-300,
'purple': $purple-300,
'pink': $pink-300,
'red': $red-300,
'orange': $orange-300,
'yellow': $yellow-300,
'green': $green-300,
'teal': $teal-300,
'cyan': $cyan-300,
'gray': $gray-300,
'black': $black,
'white': $white,
);
$theme-colors: map-merge($theme-colors, $theme-color-addons);
// Gradient
$gradient: linear-gradient(180deg, rgba($white, 0.15), rgba($white, 0));
@ -139,7 +146,7 @@ $nav-tabs-link-active-border-color: $gray-800 $gray-800 $nav-tabs-link-active-bg
$nav-pills-link-active-color: $component-active-color;
$nav-pills-link-active-bg: $component-active-bg;
$navbar-light-color: $gray-500;
$navbar-light-color: $darkest;
$navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>");
$navbar-light-toggler-border-color: $gray-700;

View File

@ -2,28 +2,47 @@
@import './theme-base.scss';
$input-border-color: $gray-200;
// Theme colors (BS5 classes)
$primary: #337ab7;
$secondary: $gray-600;
$success: $green-500;
$info: #54d6f0;
$warning: $yellow-500;
$danger: $red-500;
$light: $gray-200;
$dark: $gray-800;
$theme-colors: map-merge(
$theme-colors,
(
'primary': #337ab7,
'info': #54d6f0,
'red': $red-500,
'yellow': $yellow-500,
'green': $green-500,
$theme-colors: (
// BS5 theme colors
'primary': $primary,
'secondary': $secondary,
'success': $success,
'info': $info,
'warning': $warning,
'danger': $danger,
'light': $light,
'dark': $dark,
// General-purpose palette
'blue': $blue-500,
'cyan': $cyan-500,
'indigo': $indigo-500,
'purple': $purple-500,
'pink': $pink-500,
)
'red': $red-500,
'orange': $orange-500,
'yellow': $yellow-500,
'green': $green-500,
'teal': $teal-500,
'cyan': $cyan-500,
'gray': $gray-500,
'black': $black,
'white': $white,
);
$theme-colors: map-merge($theme-colors, $theme-color-addons);
$light: $gray-200;
$navbar-light-color: $gray-100;
$card-cap-color: $gray-800;
$accordion-bg: transparent;

View File

@ -20,7 +20,7 @@
</div>
{# Top bar #}
<nav class="navbar navbar-light sticky-top flex-md-nowrap p-1 mb-3 search container-fluid border-bottom noprint">
<nav class="navbar navbar-light sticky-top flex-md-nowrap p-1 mb-3 search container-fluid noprint">
{# Mobile Navigation #}
<div class="nav-mobile">
@ -103,6 +103,9 @@
</div>
{% endif %}
{# BS5 pop-up modals #}
{% block modals %}{% endblock %}
{# Page footer #}
<footer class="footer container-fluid">
<div class="row align-items-center justify-content-between mx-0">

View File

@ -42,5 +42,8 @@
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -42,5 +42,8 @@
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -39,5 +39,8 @@
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -42,5 +42,8 @@
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -77,5 +77,8 @@
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -39,5 +39,8 @@
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -42,5 +42,8 @@
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -42,5 +42,8 @@
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -42,5 +42,8 @@
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -4,7 +4,7 @@
{% render_errors form %}
{% block content %}
<form action="." method="post">
<form action="" method="post">
{% csrf_token %}
<div class="row mb-3">
<div class="col col-md-6 offset-md-3">

View File

@ -73,3 +73,5 @@
</div>
{% endblock content-wrapper %}
{% block modals %}{% endblock %}

View File

@ -10,7 +10,7 @@
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}">Reports</a></li>
<li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}#module.{{ report.module }}">{{ report.module|bettertitle }}</a></li>
{% endblock %}
{% endblock breadcrumbs %}
{% block subtitle %}
{% if report.description %}
@ -18,14 +18,22 @@
<div class="text-muted">{{ report.description|render_markdown }}</div>
</div>
{% endif %}
{% endblock %}
{% endblock subtitle %}
{% block controls %}{% endblock %}
{% block tabs %}{% endblock %}
{% block content-wrapper %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation">
<a href="#report" role="tab" data-bs-toggle="tab" class="nav-link active">Report</a>
</li>
</ul>
{% endblock tabs %}
{% block content %}
<div role="tabpanel" class="tab-pane active" id="report">
{% if perms.extras.run_report %}
<div class="px-3 float-end noprint">
<div class="float-end noprint">
<form action="{% url 'extras:report' module=report.module name=report.class_name %}" method="post">
{% csrf_token %}
<button type="submit" name="_run" class="btn btn-primary">
@ -38,7 +46,7 @@
</form>
</div>
{% endif %}
<div class="row px-3">
<div class="row">
<div class="col col-md-12">
{% if report.result %}
Last run: <a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">
@ -47,4 +55,5 @@
{% endif %}
</div>
</div>
{% endblock %}
</div>
{% endblock content %}

View File

@ -1,7 +1,7 @@
{% extends 'extras/report.html' %}
{% block content-wrapper %}
<div class="row px-3">
<div class="row p-3">
<div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:report_result' job_result_pk=result.pk %}" hx-trigger="every 3s"{% endif %}>
{% include 'extras/htmx/report_result.html' %}
</div>

View File

@ -7,34 +7,33 @@
{% block object_identifier %}
{{ script.full_name }}
{% endblock %}
{% endblock object_identifier %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ module }}">{{ module|bettertitle }}</a></li>
{% endblock %}
{% endblock breadcrumbs %}
{% block subtitle %}
<div class="object-subtitle">
<div class="text-muted">{{ script.Meta.description|render_markdown }}</div>
</div>
{% endblock %}
{% endblock subtitle %}
{% block controls %}{% endblock %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
<ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation">
<a href="#run" role="tab" data-bs-toggle="tab" class="nav-link active">Run</a>
</li>
<li class="nav-item" role="presentation">
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
</li>
</ul>
{% endblock %}
</ul>
{% endblock tabs %}
{% block content-wrapper %}
<div class="tab-content">
{% block content %}
<div role="tabpanel" class="tab-pane active" id="run">
<div class="row">
<div class="col">
@ -71,5 +70,4 @@
<code class="h6 my-3 d-block">{{ script.filename }}</code>
<pre class="block">{{ script.source }}</pre>
</div>
</div>
{% endblock content-wrapper %}
{% endblock content %}

View File

@ -100,4 +100,8 @@
<div class="tab-content">
{% block content %}{% endblock %}
</div>
{% endblock %}
{% endblock content-wrapper %}
{% block modals %}
{% include 'inc/htmx_modal.html' %}
{% endblock modals %}

View File

@ -1,9 +1,16 @@
{% extends 'generic/confirmation_form.html' %}
{% extends 'base/layout.html' %}
{% load form_helpers %}
{% block title %}Delete {{ obj_type }}?{% endblock %}
{% block title %}Delete {{ object_type }}?{% endblock %}
{% block message %}
<p>Are you sure you want to <strong class="text-danger">delete</strong> {{ obj_type }} <strong>{{ obj }}</strong>?</p>
{% block message_extra %}{% endblock %}
{% endblock message %}
{% block header %}{% endblock %}
{% block content %}
<div class="modal" tabindex="-1" style="display: block; position: static">
<div class="modal-dialog">
<div class="modal-content" >
{% include 'htmx/delete_form.html' %}
</div>
</div>
</div>
{% endblock %}

View File

@ -133,6 +133,8 @@
{% endif %}
</div>
{# Table config form #}
{% table_config_form table table_name="ObjectTable" %}
{% endblock content-wrapper %}
{% block modals %}
{% table_config_form table table_name="ObjectTable" %}
{% endblock modals %}

View File

@ -0,0 +1,20 @@
{% load form_helpers %}
<form action="{{ form_url }}" method="post">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title">Confirm Deletion</h5>
</div>
<div class="modal-body">
<p>Are you sure you want to <strong class="text-danger">delete</strong> {{ object_type }} <strong>{{ object }}</strong>?</p>
{% render_form form %}
</div>
<div class="modal-footer">
{% if return_url %}
<a href="{{ return_url }}" class="btn btn-outline-secondary">Cancel</a>
{% else %}
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
{% endif %}
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>

View File

@ -0,0 +1,7 @@
<div class="modal fade" id="htmx-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" id="htmx-modal-content">
{# Dynamic content goes here #}
</div>
</div>
</div>

View File

@ -37,5 +37,8 @@
</div>
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -35,5 +35,8 @@
</div>
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -35,5 +35,8 @@
</div>
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -35,5 +35,8 @@
</div>
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -37,5 +37,8 @@
</div>
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% block modals %}
{% table_config_form table %}
{% endblock modals %}

View File

@ -1,5 +0,0 @@
{% extends 'generic/object_delete.html' %}
{% block message_extra %}
<p>Note: This will <strong>not</strong> delete any child prefixes or IP addresses.</p>
{% endblock %}

View File

@ -37,9 +37,3 @@
{% endif %}
</ul>
{% endblock %}
{% block content-wrapper %}
<div class="tab-content">
{% block content %}{% endblock %}
</div>
{% endblock %}

View File

@ -5,13 +5,14 @@
<form method="post">
{% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{% table_config_form table %}
{% endblock %}
{% endblock modals %}

View File

@ -5,13 +5,14 @@
<form method="post">
{% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{% table_config_form table %}
{% endblock %}
{% endblock modals %}

View File

@ -6,13 +6,11 @@
<form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
{% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.virtualization.change_cluster %}
@ -23,5 +21,8 @@
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{% table_config_form table %}
{% endblock %}
{% endblock modals %}

View File

@ -6,13 +6,11 @@
<form method="post">
{% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.virtualization.change_virtualmachine %}
@ -28,5 +26,8 @@
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{% table_config_form table %}
{% endblock %}
{% endblock modals %}

View File

@ -37,5 +37,8 @@
<div class="clearfix"></div>
</div>
</form>
{% endblock content %}
{% block modals %}
{% table_config_form table %}
{% endblock %}
{% endblock modals %}

View File

@ -168,11 +168,11 @@ class ContentTypeChoiceMixin:
class ContentTypeChoiceField(ContentTypeChoiceMixin, forms.ModelChoiceField):
pass
widget = widgets.StaticSelect
class ContentTypeMultipleChoiceField(ContentTypeChoiceMixin, forms.ModelMultipleChoiceField):
pass
widget = widgets.StaticSelectMultiple
#

View File

@ -14,7 +14,6 @@ __all__ = (
'BulkEditNullBooleanSelect',
'ClearableFileInput',
'ColorSelect',
'ContentTypeSelect',
'DatePicker',
'DateTimePicker',
'NumericArrayField',
@ -110,15 +109,6 @@ class SelectWithPK(StaticSelect):
option_template_name = 'widgets/select_option_with_pk.html'
class ContentTypeSelect(StaticSelect):
"""
Appends an `api-value` attribute equal to the slugified model name for each ContentType. For example:
<option value="37" api-value="console-server-port">console server port</option>
This attribute can be used to reference the relevant API endpoint for a particular ContentType.
"""
option_template_name = 'widgets/select_contenttype.html'
class SelectSpeedWidget(forms.NumberInput):
"""
Speed field with dropdown selections for convenience.

View File

@ -1,3 +1,9 @@
<a href="{{ url }}" class="btn btn-sm btn-danger" role="button">
<a href="#"
hx-get="{{ url }}"
hx-target="#htmx-modal-content"
class="btn btn-sm btn-danger"
data-bs-toggle="modal"
data-bs-target="#htmx-modal"
>
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>&nbsp;Delete
</a>

View File

@ -1 +0,0 @@
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}{% if widget.value %} api-value="{{ widget.label|slugify }}"{% endif %}>{{ widget.label.label|default:widget.label|capfirst }}</option>

View File

@ -80,6 +80,12 @@ class ClusterTable(BaseTable):
name = tables.Column(
linkify=True
)
type = tables.Column(
linkify=True
)
group = tables.Column(
linkify=True
)
tenant = tables.Column(
linkify=True
)

View File

@ -3,7 +3,7 @@ from django import forms
from dcim.choices import LinkStatusChoices
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
from ipam.models import VLAN
from utilities.forms import DynamicModelChoiceField
from utilities.forms import add_blank_choice, DynamicModelChoiceField
from wireless.choices import *
from wireless.constants import SSID_MAX_LENGTH
from wireless.models import *
@ -45,24 +45,27 @@ class WirelessLANBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
label='VLAN'
)
ssid = forms.CharField(
max_length=SSID_MAX_LENGTH,
required=False
required=False,
label='SSID'
)
description = forms.CharField(
required=False
)
auth_type = forms.ChoiceField(
choices=WirelessAuthTypeChoices,
choices=add_blank_choice(WirelessAuthTypeChoices),
required=False
)
auth_cipher = forms.ChoiceField(
choices=WirelessAuthCipherChoices,
choices=add_blank_choice(WirelessAuthCipherChoices),
required=False
)
auth_psk = forms.CharField(
required=False
required=False,
label='Pre-shared key'
)
class Meta:
@ -76,25 +79,27 @@ class WirelessLinkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
)
ssid = forms.CharField(
max_length=SSID_MAX_LENGTH,
required=False
required=False,
label='SSID'
)
status = forms.ChoiceField(
choices=LinkStatusChoices,
choices=add_blank_choice(LinkStatusChoices),
required=False
)
description = forms.CharField(
required=False
)
auth_type = forms.ChoiceField(
choices=WirelessAuthTypeChoices,
choices=add_blank_choice(WirelessAuthTypeChoices),
required=False
)
auth_cipher = forms.ChoiceField(
choices=WirelessAuthCipherChoices,
choices=add_blank_choice(WirelessAuthCipherChoices),
required=False
)
auth_psk = forms.CharField(
required=False
required=False,
label='Pre-shared key'
)
class Meta:

View File

@ -1,4 +1,4 @@
Django==3.2.10
Django==3.2.11
django-cors-headers==3.10.1
django-debug-toolbar==3.2.4
django-filter==21.1
@ -18,7 +18,7 @@ gunicorn==20.1.0
Jinja2==3.0.3
Markdown==3.3.6
markdown-include==0.6.0
mkdocs-material==8.1.3
mkdocs-material==8.1.4
netaddr==0.8.0
Pillow==8.4.0
psycopg2-binary==2.9.3