From b652e07d76b1a2626449bb74c94d96969eed896c Mon Sep 17 00:00:00 2001 From: Italo Valcy Date: Sat, 11 Jul 2026 06:40:28 -0300 Subject: [PATCH 1/3] fix: adidng safe guards around saveAnswers to avoid unsaved answers --- apps/templates/pages/lab_instance_view.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/templates/pages/lab_instance_view.html b/apps/templates/pages/lab_instance_view.html index d2cdf34..1dbba31 100644 --- a/apps/templates/pages/lab_instance_view.html +++ b/apps/templates/pages/lab_instance_view.html @@ -410,6 +410,16 @@

Lab Guide

if (!answers || Object.keys(answers).length === 0) { return; } + // Never overwrite stored answers with an all-blank form. Firefox's + // form-state restoration re-blanks fields after load and fires change + // events; without this guard that would auto-save an empty payload and + // wipe the user's answers. + var hasValue = Object.keys(answers).some(function(k){ + return answers[k] !== "" && answers[k] != null; + }); + if (!hasValue) { + return; + } var request = $.ajax({ url: "/api/lab_answers/{{ lab['lab_instance_id'] }}", type: "POST", @@ -441,6 +451,9 @@

Lab Guide

$('.save-answers').click(function () { saveAnswers(); }); + // stop Firefox from restoring/re-blanking these fields after load, which + // otherwise fights the answer loader and triggers empty auto-saves + $("#lab-guide input, #lab-guide textarea, #lab-guide select").attr('autocomplete', 'off'); var timeoutSave; $("#lab-guide input, #lab-guide textarea, #lab-guide select").on('input propertychange change', function() { // don't auto-save until the initial load has settled, otherwise a From d4d5cfd7f05dda125253924edb16d504ffbd526a Mon Sep 17 00:00:00 2001 From: Italo Valcy Date: Sat, 11 Jul 2026 06:45:08 -0300 Subject: [PATCH 2/3] adding extra logging for save answers --- apps/api/routes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/api/routes.py b/apps/api/routes.py index 1e4573b..76ad0d8 100644 --- a/apps/api/routes.py +++ b/apps/api/routes.py @@ -196,6 +196,11 @@ def save_lab_answers(lab_inst_id): lab_answers = LabAnswers(user_id=current_user.id, lab_id=lab_inst.lab_id) db.session.add(lab_answers) + current_app.logger.info( + f"Save lab_answers id={lab_answers.id} user={current_user.username} " + f"lab={lab_inst.lab_id} request-answers={content}" + ) + # Merge into the stored answers rather than replacing them: keep keys not # present in this payload, and don't let an empty incoming value wipe an # answer that was already saved (guards against a partial/early auto-save From b71514fbd38bed14c01c0002c8c79b0779e02bf2 Mon Sep 17 00:00:00 2001 From: Italo Valcy Date: Sat, 11 Jul 2026 06:49:45 -0300 Subject: [PATCH 3/3] update regrassion tests accordingly --- tests/js/labAnswers.test.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/js/labAnswers.test.js b/tests/js/labAnswers.test.js index 5dfcb2e..85873fe 100644 --- a/tests/js/labAnswers.test.js +++ b/tests/js/labAnswers.test.js @@ -147,11 +147,24 @@ const { $ } = env; eq('save: first radio not clobbered by later unchecked siblings', post.q_radio, 'A'); } -// === saveAnswers: empty / unchecked groups serialize as "" ================= +// === saveAnswers: a fully-blank form is NOT posted (never wipe answers) ===== +// Firefox re-blanks the guide inputs after load and fires change events; the +// guard in saveAnswers must skip the POST entirely so an all-empty payload +// can't overwrite previously saved answers. { env.setForm(FORM); const post = env.save(); - eq('save: empty text', post.q_text, ''); + eq('save: blank form suppresses POST', post, undefined); +} + +// === saveAnswers: blank fields still serialize as "" in a partial save ====== +// When at least one field has a value the POST goes through, and the empty +// fields must still serialize as "" so they round-trip via applyAnswers. +{ + env.setForm(FORM); + $("input[name=q_text]").val('answered'); + const post = env.save(); + eq('save: partial POST happens', post && post.q_text, 'answered'); eq('save: unchecked checkbox group', post.q_cb, ''); eq('save: no radio selected', post.q_radio, ''); }