Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
4cd514c861 Closes #20614: Update ruff for pre-commit check 2025-10-20 10:17:00 -04:00
19 changed files with 243 additions and 418 deletions

View File

@@ -14,16 +14,16 @@ from netbox.filtersets import (
AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, NetBoxModelFilterSet,
OrganizationalModelFilterSet,
)
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import *
from users.models import User
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
from virtualization.models import Cluster, ClusterGroup, VMInterface, VirtualMachine
from vpn.models import L2VPN
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from wireless.models import WirelessLAN, WirelessLink
from .choices import *
from .constants import *
@@ -1807,14 +1807,6 @@ class MACAddressFilterSet(NetBoxModelFilterSet):
queryset=VMInterface.objects.all(),
label=_('VM interface (ID)'),
)
assigned = django_filters.BooleanFilter(
method='filter_assigned',
label=_('Is assigned'),
)
primary = django_filters.BooleanFilter(
method='filter_primary',
label=_('Is primary'),
)
class Meta:
model = MACAddress
@@ -1851,29 +1843,6 @@ class MACAddressFilterSet(NetBoxModelFilterSet):
vminterface__in=interface_ids
)
def filter_assigned(self, queryset, name, value):
params = {
'assigned_object_type__isnull': True,
'assigned_object_id__isnull': True,
}
if value:
return queryset.exclude(**params)
else:
return queryset.filter(**params)
def filter_primary(self, queryset, name, value):
interface_mac_ids = Interface.objects.filter(primary_mac_address_id__isnull=False).values_list(
'primary_mac_address_id', flat=True
)
vminterface_mac_ids = VMInterface.objects.filter(primary_mac_address_id__isnull=False).values_list(
'primary_mac_address_id', flat=True
)
query = Q(pk__in=interface_mac_ids) | Q(pk__in=vminterface_mac_ids)
if value:
return queryset.filter(query)
else:
return queryset.exclude(query)
class CommonInterfaceFilterSet(django_filters.FilterSet):
mode = django_filters.MultipleChoiceFilter(

View File

@@ -1676,16 +1676,12 @@ class MACAddressFilterForm(NetBoxModelFilterSetForm):
model = MACAddress
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('mac_address', name=_('Attributes')),
FieldSet(
'device_id', 'virtual_machine_id', 'assigned', 'primary',
name=_('Assignments'),
),
FieldSet('mac_address', 'device_id', 'virtual_machine_id', name=_('MAC address')),
)
selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
mac_address = forms.CharField(
required=False,
label=_('MAC address'),
label=_('MAC address')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@@ -1697,20 +1693,6 @@ class MACAddressFilterForm(NetBoxModelFilterSetForm):
required=False,
label=_('Assigned VM'),
)
assigned = forms.NullBooleanField(
required=False,
label=_('Assigned to an interface'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
),
)
primary = forms.NullBooleanField(
required=False,
label=_('Primary MAC of an interface'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
),
)
tag = TagFilterField(model)

View File

@@ -755,10 +755,7 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
queryset=ModuleBay.objects.all(),
query_params={
'device_id': '$device'
},
context={
'disabled': 'installed_module',
},
}
)
module_type = DynamicModelChoiceField(
label=_('Module type'),

View File

@@ -18,9 +18,7 @@ from netbox.graphql.filter_mixins import (
ImageAttachmentFilterMixin,
WeightFilterMixin,
)
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from virtualization.models import VMInterface
from tenancy.graphql.filter_mixins import TenancyFilterMixin, ContactFilterMixin
from .filter_mixins import (
CabledObjectModelFilterMixin,
ComponentModelFilterMixin,
@@ -421,24 +419,6 @@ class MACAddressFilter(PrimaryModelFilterMixin):
)
assigned_object_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter_field()
def assigned(self, value: bool, prefix) -> Q:
return Q(**{f'{prefix}assigned_object_id__isnull': (not value)})
@strawberry_django.filter_field()
def primary(self, value: bool, prefix) -> Q:
interface_mac_ids = models.Interface.objects.filter(primary_mac_address_id__isnull=False).values_list(
'primary_mac_address_id', flat=True
)
vminterface_mac_ids = VMInterface.objects.filter(primary_mac_address_id__isnull=False).values_list(
'primary_mac_address_id', flat=True
)
query = Q(**{f'{prefix}pk__in': interface_mac_ids}) | Q(**{f'{prefix}pk__in': vminterface_mac_ids})
if value:
return Q(query)
else:
return ~Q(query)
@strawberry_django.filter_type(models.Interface, lookups=True)
class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):

View File

@@ -1174,9 +1174,6 @@ class MACAddressTable(NetBoxTable):
orderable=False,
verbose_name=_('Parent')
)
is_primary = columns.BooleanColumn(
verbose_name=_('Primary')
)
tags = columns.TagColumn(
url_name='dcim:macaddress_list'
)
@@ -1187,7 +1184,7 @@ class MACAddressTable(NetBoxTable):
class Meta(DeviceComponentTable.Meta):
model = models.MACAddress
fields = (
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'is_primary',
'comments', 'tags', 'created', 'last_updated',
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'comments', 'tags',
'created', 'last_updated',
)
default_columns = ('pk', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description')

View File

@@ -10,7 +10,7 @@ from netbox.choices import ColorChoices, WeightUnitChoices
from tenancy.models import Tenant, TenantGroup
from users.models import User
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
from virtualization.models import Cluster, ClusterType, ClusterGroup, VMInterface, VirtualMachine
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
from wireless.models import WirelessLink
@@ -7164,20 +7164,9 @@ class MACAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
MACAddress(mac_address='00-00-00-05-01-01', assigned_object=vm_interfaces[1]),
MACAddress(mac_address='00-00-00-06-01-01', assigned_object=vm_interfaces[2]),
MACAddress(mac_address='00-00-00-06-01-02', assigned_object=vm_interfaces[2]),
# unassigned
MACAddress(mac_address='00-00-00-07-01-01'),
)
MACAddress.objects.bulk_create(mac_addresses)
# Set MAC addresses as primary
for idx, interface in enumerate(interfaces):
interface.primary_mac_address = mac_addresses[idx]
interface.save()
for idx, vm_interface in enumerate(vm_interfaces):
# Offset by 4 for device MACs
vm_interface.primary_mac_address = mac_addresses[idx + 4]
vm_interface.save()
def test_mac_address(self):
params = {'mac_address': ['00-00-00-01-01-01', '00-00-00-02-01-01']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -7209,15 +7198,3 @@ class MACAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'vminterface': [vm_interfaces[0].name, vm_interfaces[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_assigned(self):
params = {'assigned': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
params = {'assigned': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_primary(self):
params = {'primary': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'primary': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)

View File

@@ -535,15 +535,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# URL
elif self.type == CustomFieldTypeChoices.TYPE_URL:
field = LaxURLField(assume_scheme='https', required=required, initial=initial)
if self.validation_regex:
field.validators = [
RegexValidator(
regex=self.validation_regex,
message=mark_safe(_("Values must match this regex: <code>{regex}</code>").format(
regex=escape(self.validation_regex)
))
)
]
# JSON
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
@@ -693,13 +684,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
if self.validation_regex and not re.match(self.validation_regex, value):
raise ValidationError(_("Value must match regex '{regex}'").format(regex=self.validation_regex))
# Validate URL field
elif self.type == CustomFieldTypeChoices.TYPE_URL:
if type(value) is not str:
raise ValidationError(_("Value must be a string."))
if self.validation_regex and not re.match(self.validation_regex, value):
raise ValidationError(_("Value must match regex '{regex}'").format(regex=self.validation_regex))
# Validate integer
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
if type(value) is not int:

View File

@@ -3,7 +3,6 @@ import importlib.util
import os
import sys
from django.core.cache import cache
from django.core.files.storage import storages
from django.db import models
from django.http import HttpResponse
@@ -31,14 +30,7 @@ class CustomStoragesLoader(importlib.abc.Loader):
return None # Use default module creation
def exec_module(self, module):
# Cache storage for 5 minutes (300 seconds)
cache_key = "storage_scripts"
storage = cache.get(cache_key)
if storage is None:
storage = storages['scripts']
cache.set(cache_key, storage, timeout=300) # 5 minutes
storage = storages.create_storage(storages.backends["scripts"])
with storage.open(self.filename, 'rb') as f:
code = f.read()
exec(code, module.__dict__)

View File

@@ -1300,28 +1300,6 @@ class CustomFieldAPITest(APITestCase):
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
def test_url_regex_validation(self):
"""
Test that validation_regex is applied to URL custom fields (fixes #20498).
"""
site2 = Site.objects.get(name='Site 2')
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
self.add_permissions('dcim.change_site')
cf_url = CustomField.objects.get(name='url_field')
cf_url.validation_regex = r'^https://' # Require HTTPS
cf_url.save()
# Test invalid URL (http instead of https)
data = {'custom_fields': {'url_field': 'http://example.com'}}
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
# Test valid URL (https)
data = {'custom_fields': {'url_field': 'https://example.com'}}
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
def test_uniqueness_validation(self):
# Create a unique custom field
cf_text = CustomField.objects.get(name='text_field')

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

@@ -20,13 +20,11 @@ function slugify(slug: string, chars: number): string {
* For any slug fields, add event listeners to handle automatically generating slug values.
*/
export function initReslug(): void {
for (const slugButton of getElements<HTMLButtonElement>('button.reslug')) {
for (const slugButton of getElements<HTMLButtonElement>('button#reslug')) {
const form = slugButton.form;
if (form == null) continue;
const slugField = form.querySelector('input.slug-field') as HTMLInputElement;
const slugField = form.querySelector('#id_slug') as HTMLInputElement;
if (slugField == null) continue;
const sourceId = slugField.getAttribute('slug-source');
const sourceField = form.querySelector(`#id_${sourceId}`) as HTMLInputElement;

View File

@@ -16,11 +16,6 @@ pre {
background: var(--#{$prefix}bg-surface);
}
// Permit copying of badge text
.badge {
user-select: text;
}
// Button adjustments
.btn {
// Tabler sets display: flex

View File

@@ -37,23 +37,23 @@
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow" {% htmx_boost %}>
<a href="{% url 'account:profile' %}" class="dropdown-item">
<i class="dropdown-item-icon mdi mdi-account"></i> {% trans "Profile" %}
<i class="mdi mdi-account"></i> {% trans "Profile" %}
</a>
<a href="{% url 'account:bookmarks' %}" class="dropdown-item">
<i class="dropdown-item-icon mdi mdi-bookmark"></i> {% trans "Bookmarks" %}
<i class="mdi mdi-bookmark"></i> {% trans "Bookmarks" %}
</a>
<a href="{% url 'account:subscriptions' %}" class="dropdown-item">
<i class="dropdown-item-icon mdi mdi-bell"></i> {% trans "Subscriptions" %}
<i class="mdi mdi-bell"></i> {% trans "Subscriptions" %}
</a>
<a href="{% url 'account:preferences' %}" class="dropdown-item">
<i class="dropdown-item-icon mdi mdi-wrench"></i> {% trans "Preferences" %}
<i class="mdi mdi-wrench"></i> {% trans "Preferences" %}
</a>
<a href="{% url 'account:usertoken_list' %}" class="dropdown-item">
<i class="dropdown-item-icon mdi mdi-key"></i> {% trans "API Tokens" %}
<i class="mdi mdi-key"></i> {% trans "API Tokens" %}
</a>
<hr class="dropdown-divider" />
<a href="{% url 'logout' %}" hx-disable="true" class="dropdown-item">
<i class="dropdown-item-icon mdi mdi-logout-variant"></i> {% trans "Log Out" %}
<i class="mdi mdi-logout-variant"></i> {% trans "Log Out" %}
</a>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -53,14 +53,6 @@ class SlugField(forms.SlugField):
self.widget.attrs['slug-source'] = slug_source
def get_bound_field(self, form, field_name):
if prefix := form.prefix:
slug_source = self.widget.attrs.get('slug-source')
if slug_source and not slug_source.startswith(f'{prefix}-'):
self.widget.attrs['slug-source'] = f"{prefix}-{slug_source}"
return super().get_bound_field(form, field_name)
class ColorField(forms.CharField):
"""

View File

@@ -56,14 +56,6 @@ class SlugWidget(forms.TextInput):
"""
template_name = 'widgets/sluginput.html'
def __init__(self, attrs=None):
local_attrs = {} if attrs is None else attrs.copy()
if 'class' in local_attrs:
local_attrs['class'] = f"{local_attrs['class']} slug-field"
else:
local_attrs['class'] = 'slug-field'
super().__init__(local_attrs)
class ArrayWidget(forms.Textarea):
"""

View File

@@ -19,7 +19,7 @@
{% if field|widget_type == 'slugwidget' %}
<div class="input-group">
{{ field }}
<button type="button" title="{% trans "Regenerate Slug" %}" class="btn reslug">
<button id="reslug" type="button" title="{% trans "Regenerate Slug" %}" class="btn">
<i class="mdi mdi-reload"></i>
</button>
</div>