Compare commits

...

5 Commits

Author SHA1 Message Date
Jeremy Stretch
3bd0186870 Fixes #20637: Omit inventory item serials from device search filter to improve performance 2025-10-23 10:11:08 -04:00
bctiemann
09ce8a808d Merge pull request #20651 from netbox-community/19872-script-validation-errors
Fixes #19872: Display script form validation errors
2025-10-23 09:59:29 -04:00
github-actions
cb3308a166 Update source translation strings 2025-10-22 05:02:23 +00:00
Jason Novinger
5fbae8407e Only show non-rendered field errors in toast
When script form validation fails, display error messages for fields not
in fieldsets. Fields in fieldsets show inline errors only; hidden fields
show toast notifications to provide feedback instead of failing silently.
2025-10-21 11:54:46 -05:00
Jason Novinger
2fdd46f64c Fixes #19872: Display form validation errors for script execution
When script form validation fails (e.g., required fields excluded from
fieldsets), display error messages via Django's message framework instead
of failing silently. Error format: "field: error1, error2; field2: error".
2025-10-21 11:16:56 -05:00
4 changed files with 83 additions and 5 deletions

View File

@@ -1288,7 +1288,6 @@ class DeviceFilterSet(
Q(name__icontains=value) |
Q(virtual_chassis__name__icontains=value) |
Q(serial__icontains=value.strip()) |
Q(inventoryitems__serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
Q(description__icontains=value.strip()) |
Q(comments__icontains=value) |

View File

@@ -1,11 +1,14 @@
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.test import tag
from core.choices import ManagedFileRootPathChoices
from core.events import *
from core.models import ObjectType
from dcim.models import DeviceType, Manufacturer, Site
from extras.choices import *
from extras.models import *
from extras.scripts import Script as PythonClass, IntegerVar, BooleanVar
from users.models import Group, User
from utilities.testing import ViewTestCases, TestCase
@@ -897,3 +900,70 @@ class ScriptListViewTest(TestCase):
response = self.client.get(url, {'embedded': 'true'})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'extras/inc/script_list_content.html')
class ScriptValidationErrorTest(TestCase):
user_permissions = ['extras.view_script', 'extras.run_script']
class TestScriptMixin:
bar = IntegerVar(min_value=0, max_value=30, default=30)
class TestScriptClass(TestScriptMixin, PythonClass):
class Meta:
name = 'Test script'
commit_default = False
fieldsets = (("Logging", ("debug_mode",)),)
debug_mode = BooleanVar(default=False)
def run(self, data, commit):
return "Complete"
@classmethod
def setUpTestData(cls):
module = ScriptModule.objects.create(file_root=ManagedFileRootPathChoices.SCRIPTS, file_path='test_script.py')
cls.script = Script.objects.create(module=module, name='Test script', is_executable=True)
def setUp(self):
super().setUp()
Script.python_class = property(lambda self: ScriptValidationErrorTest.TestScriptClass)
@tag('regression')
def test_script_validation_error_displays_message(self):
from unittest.mock import patch
url = reverse('extras:script', kwargs={'pk': self.script.pk})
with patch('extras.views.get_workers_for_queue', return_value=['worker']):
response = self.client.post(url, {'debug_mode': 'true', '_commit': 'true'})
self.assertEqual(response.status_code, 200)
messages = list(response.context['messages'])
self.assertEqual(len(messages), 1)
self.assertEqual(str(messages[0]), "bar: This field is required.")
@tag('regression')
def test_script_validation_error_no_toast_for_fieldset_fields(self):
from unittest.mock import patch, PropertyMock
class FieldsetScript(PythonClass):
class Meta:
name = 'Fieldset test'
commit_default = False
fieldsets = (("Fields", ("required_field",)),)
required_field = IntegerVar(min_value=10)
def run(self, data, commit):
return "Complete"
url = reverse('extras:script', kwargs={'pk': self.script.pk})
with patch.object(Script, 'python_class', new_callable=PropertyMock) as mock_python_class:
mock_python_class.return_value = FieldsetScript
with patch('extras.views.get_workers_for_queue', return_value=['worker']):
response = self.client.post(url, {'required_field': '5', '_commit': 'true'})
self.assertEqual(response.status_code, 200)
messages = list(response.context['messages'])
self.assertEqual(len(messages), 0)

View File

@@ -1485,6 +1485,15 @@ class ScriptView(BaseScriptView):
)
return redirect('extras:script_result', job_pk=job.pk)
else:
fieldset_fields = {field for _, fields in script_class.get_fieldsets() for field in fields}
hidden_errors = {
field: errors for field, errors in form.errors.items()
if field not in fieldset_fields
}
if hidden_errors:
error_msg = '; '.join(f"{field}: {', '.join(errors)}" for field, errors in hidden_errors.items())
messages.error(request, error_msg)
return render(request, 'extras/script.html', {
'object': script,

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-21 05:03+0000\n"
"POT-Creation-Date: 2025-10-22 05:02+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"
@@ -4162,7 +4162,7 @@ msgstr ""
#: netbox/templates/circuits/inc/circuit_termination.html:32
#: netbox/templates/generic/bulk_edit.html:65
#: netbox/templates/inc/panels/tags.html:5
#: netbox/utilities/forms/fields/fields.py:83
#: netbox/utilities/forms/fields/fields.py:91
msgid "Tags"
msgstr ""
@@ -16033,11 +16033,11 @@ msgstr ""
msgid "URL-friendly unique shorthand"
msgstr ""
#: netbox/utilities/forms/fields/fields.py:106
#: netbox/utilities/forms/fields/fields.py:114
msgid "Enter context data in <a href=\"https://json.org/\">JSON</a> format."
msgstr ""
#: netbox/utilities/forms/fields/fields.py:127
#: netbox/utilities/forms/fields/fields.py:135
msgid "MAC address must be in EUI-48 format"
msgstr ""