mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
fix(instances): a refused VM create form comes back with what was entered
When the server refused a VM create submission (a field error, a busy resource, a reused submission key, an existing VM name, a missing MAC for a new disk), the page came back on its first tab with the flavor dialog closed and every entered value gone: the hand-written fields showed fixed defaults, and the errors sat in a block at the top of the page, outside #messages and tied to no field. The view builds one NewVMForm per create form on the page (custom, template, one per flavor; create_forms), each with its own defaults; a hidden form_id, which is also the prefix of the form's field ids, names the submitted one, and it comes back bound. Every field renders from its own form and is marked by two small template tags (form_fields: error_attrs, field_error) when refused; a summary at the top of the form lists all of its errors, also those of fields it keeps hidden. The submitted tab or flavor dialog, marked data-form-id, reopens with any "Advanced" section that holds an error; a reopened dialog shows the messages too; the template form gets its pool, template and storage back. base.html's error focus is a function that walks the places an error can be, and a page can call it once it is set up, as the create page does. form_id changes nothing in how a POST is processed; one of an unknown form (a flavor deleted since) has its errors in the messages. The default video now applies to every create form. The custom form's disks and networks are not restored yet.
This commit is contained in:
parent
35468bc0fd
commit
dba41f97f3
9 changed files with 640 additions and 164 deletions
|
|
@ -12,7 +12,7 @@ from pathlib import Path
|
|||
from django.conf import settings
|
||||
|
||||
SCRIPT = re.search(
|
||||
r"<script>\s*(// after a refused submission.*?)</script>",
|
||||
r"<script>\s*(// After a refused submission.*?)</script>",
|
||||
Path(settings.BASE_DIR, "templates/base.html").read_text(),
|
||||
re.S,
|
||||
).group(1)
|
||||
|
|
@ -20,30 +20,45 @@ SCRIPT = re.search(
|
|||
HARNESS = """
|
||||
const els = %s;
|
||||
let focused = null;
|
||||
for (const e of els) e.focus = () => { focused = e; };
|
||||
for (const e of els) { e.focus = () => { focused = e; }; e.matches = () => Boolean(e.field); }
|
||||
const document = {
|
||||
body: {dataset: %s},
|
||||
querySelectorAll: sel => els.filter(e => e.sel === sel),
|
||||
querySelector: sel => els.find(e => e.sel === sel) || null,
|
||||
};
|
||||
const window = globalThis;
|
||||
%s
|
||||
if (%s) window.focusFirstError();
|
||||
console.log(JSON.stringify({focused: focused && focused.name, tabIndex: focused && focused.tabIndex !== undefined ? focused.tabIndex : null}));
|
||||
"""
|
||||
|
||||
|
||||
class ErrorFocusTests(unittest.TestCase):
|
||||
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
||||
def focused(self, els, dataset=None, call=False):
|
||||
script = HARNESS % (json.dumps(els), json.dumps(dataset or {}), SCRIPT, "true" if call else "false")
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
||||
def test_focus_goes_to_the_error(self):
|
||||
invalid = {"sel": ".is-invalid", "offsetParent": {}, "name": "field"}
|
||||
hidden = {"sel": ".is-invalid", "offsetParent": None, "name": "hidden"}
|
||||
invalid = {"sel": ".is-invalid", "offsetParent": {}, "name": "field", "field": True}
|
||||
hidden = {"sel": ".is-invalid", "offsetParent": None, "name": "hidden", "field": True}
|
||||
summary = {"sel": ".form-errors", "offsetParent": {}, "name": "summary"}
|
||||
alert = {"sel": "#messages .alert-danger", "offsetParent": {}, "name": "message"}
|
||||
in_modal = {"sel": ".modal.show .modal-messages .alert-danger", "offsetParent": {}, "name": "modal message"}
|
||||
for els, expected in (
|
||||
([hidden, invalid, alert], {"focused": "field", "tabIndex": None}),
|
||||
([hidden, invalid, summary, alert], {"focused": "field", "tabIndex": None}),
|
||||
([hidden, summary, in_modal, alert], {"focused": "summary", "tabIndex": -1}), # a hidden field's error
|
||||
([hidden, alert], {"focused": "message", "tabIndex": -1}),
|
||||
([hidden, in_modal, alert], {"focused": "modal message", "tabIndex": -1}), # the page's copy is behind the dialog
|
||||
([], {"focused": None, "tabIndex": None}),
|
||||
):
|
||||
with self.subTest(expected=expected["focused"]):
|
||||
result = subprocess.run(
|
||||
["node", "-e", HARNESS % (json.dumps(els), SCRIPT)], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(json.loads(result.stdout), expected)
|
||||
self.assertEqual(self.focused(els), expected)
|
||||
|
||||
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
||||
def test_a_page_can_focus_the_error_itself(self):
|
||||
invalid = {"sel": ".is-invalid", "offsetParent": {}, "name": "field", "field": True}
|
||||
self.assertEqual(self.focused([invalid], {"errorFocus": "manual"}), {"focused": None, "tabIndex": None})
|
||||
self.assertEqual(self.focused([invalid], {"errorFocus": "manual"}, call=True), {"focused": "field", "tabIndex": None})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue