Compare commits

..

2 Commits

Author SHA1 Message Date
Arthur
0e8023899f #20383 clear rack face if no rack on edit 2026-01-15 09:38:00 -08:00
Arthur
601a7092e0 #20383 clear rack face if no rack on edit 2026-01-15 09:20:38 -08:00
11 changed files with 56 additions and 116 deletions

View File

@@ -44,4 +44,3 @@ class DataFileSerializer(NetBoxModelSerializer):
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
]
brief_fields = ('id', 'url', 'display', 'path')
read_only_fields = ['path', 'last_updated', 'size', 'hash']

View File

@@ -12,7 +12,7 @@ from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext as _
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
from netbox.models import PrimaryModel
@@ -128,9 +128,7 @@ class DataSource(JobsMixin, PrimaryModel):
# Ensure URL scheme matches selected type
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
raise ValidationError({
'source_url': _("URLs for local sources must start with {scheme} (or specify no scheme)").format(
scheme='file://'
)
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
})
def save(self, *args, **kwargs):

View File

@@ -722,6 +722,9 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
if position:
self.fields['position'].widget.choices = [(position, f'U{position}')]
# Clear face field when rack is cleared
self.fields['face'].widget.attrs['ts-clear-field'] = 'rack'
class ModuleForm(ModuleCommonForm, PrimaryModelForm):
device = DynamicModelChoiceField(

View File

@@ -28,7 +28,7 @@ class ConfigContextProfileSerializer(PrimaryModelSerializer):
)
data_file = DataFileSerializer(
nested=True,
required=False
read_only=True
)
class Meta:
@@ -143,7 +143,7 @@ class ConfigContextSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedM
)
data_file = DataFileSerializer(
nested=True,
required=False
read_only=True
)
class Meta:

View File

@@ -22,7 +22,7 @@ from utilities.forms.fields import (
DynamicModelMultipleChoiceField, JSONField, SlugField,
)
from utilities.forms.rendering import FieldSet, ObjectAttribute
from utilities.forms.widgets import ChoicesWidget, ClearableFileInput, HTMXSelect
from utilities.forms.widgets import ChoicesWidget, HTMXSelect
from utilities.tables import get_table_for_model
from virtualization.models import Cluster, ClusterGroup, ClusterType
@@ -784,10 +784,6 @@ class ImageAttachmentForm(forms.ModelForm):
fields = [
'image', 'name', 'description',
]
# Explicitly set 'image/avif' to support AVIF selection in Firefox
widgets = {
'image': ClearableFileInput(attrs={'accept': 'image/*,image/avif'}),
}
help_texts = {
'name': _("If no name is specified, the file name will be used.")
}

View File

@@ -1,5 +1,4 @@
import datetime
import hashlib
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
@@ -8,7 +7,7 @@ from rest_framework import status
from core.choices import ManagedFileRootPathChoices
from core.events import *
from core.models import DataFile, DataSource, ObjectType
from core.models import ObjectType
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
from extras.choices import *
from extras.models import *
@@ -732,51 +731,6 @@ class ConfigContextProfileTest(APIViewTestCases.APIViewTestCase):
)
ConfigContextProfile.objects.bulk_create(profiles)
def test_update_data_source_and_data_file(self):
"""
Regression test: Ensure data_source and data_file can be assigned via the API.
This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
"""
self.add_permissions(
'core.view_datafile',
'core.view_datasource',
'extras.view_configcontextprofile',
'extras.change_configcontextprofile',
)
config_context_profile = ConfigContextProfile.objects.first()
# Create a data source and file
datasource = DataSource.objects.create(
name='Data Source 1',
type='local',
source_url='file:///tmp/netbox-datasource/',
)
# Generate a valid dummy YAML file
file_data = b'profile: configcontext\n'
datafile = DataFile.objects.create(
source=datasource,
path='dir1/file1.yml',
last_updated=now(),
size=len(file_data),
hash=hashlib.sha256(file_data).hexdigest(),
data=file_data,
)
url = self._get_detail_url(config_context_profile)
payload = {
'data_source': datasource.pk,
'data_file': datafile.pk,
}
response = self.client.patch(url, payload, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
config_context_profile.refresh_from_db()
self.assertEqual(config_context_profile.data_source_id, datasource.pk)
self.assertEqual(config_context_profile.data_file_id, datafile.pk)
self.assertEqual(response.data['data_source']['id'], datasource.pk)
self.assertEqual(response.data['data_file']['id'], datafile.pk)
class ConfigContextTest(APIViewTestCases.APIViewTestCase):
model = ConfigContext
@@ -858,51 +812,6 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
rendered_context = device.get_config_context()
self.assertEqual(rendered_context['bar'], 456)
def test_update_data_source_and_data_file(self):
"""
Regression test: Ensure data_source and data_file can be assigned via the API.
This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
"""
self.add_permissions(
'core.view_datafile',
'core.view_datasource',
'extras.view_configcontext',
'extras.change_configcontext',
)
config_context = ConfigContext.objects.first()
# Create a data source and file
datasource = DataSource.objects.create(
name='Data Source 1',
type='local',
source_url='file:///tmp/netbox-datasource/',
)
# Generate a valid dummy YAML file
file_data = b'context: config\n'
datafile = DataFile.objects.create(
source=datasource,
path='dir1/file1.yml',
last_updated=now(),
size=len(file_data),
hash=hashlib.sha256(file_data).hexdigest(),
data=file_data,
)
url = self._get_detail_url(config_context)
payload = {
'data_source': datasource.pk,
'data_file': datafile.pk,
}
response = self.client.patch(url, payload, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
config_context.refresh_from_db()
self.assertEqual(config_context.data_source_id, datasource.pk)
self.assertEqual(config_context.data_file_id, datafile.pk)
self.assertEqual(response.data['data_source']['id'], datasource.pk)
self.assertEqual(response.data['data_file']['id'], datafile.pk)
class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConfigTemplate

View File

@@ -78,7 +78,7 @@ def image_upload(instance, filename):
"""
upload_dir = 'image-attachments'
default_filename = 'unnamed'
allowed_img_extensions = ('avif', 'bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp')
allowed_img_extensions = ('bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp')
# Normalize Windows paths and create a Path object.
normalized_filename = str(filename).replace('\\', '/')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,4 @@
import TomSelect from 'tom-select';
import { getElements } from '../util';
function handleFormSubmit(): void {
@@ -8,6 +9,37 @@ function handleFormSubmit(): void {
}
}
/**
* Initialize clear-field dependencies.
* When a field with ts-clear-field attribute's parent field is cleared, this field will also be cleared.
*/
function initClearFieldDependencies(): void {
// Find all fields with ts-clear-field attribute
for (const field of getElements<HTMLSelectElement>('[ts-clear-field]')) {
const parentFieldName = field.getAttribute('ts-clear-field');
if (!parentFieldName) continue;
// Find the parent field
const parentField = document.querySelector<HTMLSelectElement>(`[name="${parentFieldName}"]`);
if (!parentField) continue;
// Listen for changes on the parent field
parentField.addEventListener('change', () => {
// If parent field is cleared, also clear this dependent field
if (!parentField.value || parentField.value === '') {
// Check if this field uses TomSelect
const tomselect = (field as HTMLSelectElement & { tomselect?: TomSelect }).tomselect;
if (tomselect) {
tomselect.clear();
} else {
// Regular select field
field.value = '';
}
}
});
}
}
/**
* Attach event listeners to each form's submit/reset buttons.
*/
@@ -28,4 +60,7 @@ export function initFormElements(): void {
});
}
}
// Initialize clear-field dependencies
initClearFieldDependencies();
}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-15 05:05+0000\n"
"POT-Creation-Date: 2026-01-13 05:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -12489,8 +12489,8 @@ msgstr ""
msgid "Delete Selected"
msgstr ""
#: netbox/netbox/plugins/navigation.py:53
#: netbox/netbox/plugins/navigation.py:89
#: netbox/netbox/plugins/navigation.py:55
#: netbox/netbox/plugins/navigation.py:88
msgid "Permissions must be passed as a tuple or list."
msgstr ""
@@ -12498,7 +12498,7 @@ msgstr ""
msgid "Buttons must be passed as a tuple or list."
msgstr ""
#: netbox/netbox/plugins/navigation.py:95
#: netbox/netbox/plugins/navigation.py:92
msgid "Button color must be a choice within ButtonColorChoices."
msgstr ""