From 2b1d84c7043d28b68e02f1e00d03864e6bc2f341 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Thu, 23 Jul 2026 13:46:57 -0400 Subject: [PATCH 1/2] Converted party-type, confidentiality, and 'other side' party-type fields from dropdowns to radio buttons across the filing flow, plus fixed a caching bug hiding the changes. Fix #130 --- efile_app/efile/settings_base.py | 1 + .../efile/static/js/cascading-dropdowns.js | 73 +++++++++- .../efile/static/js/dynamic-form-sections.js | 125 +++++++++++------- efile_app/efile/static/js/form-validation.js | 14 +- efile_app/efile/static/js/upload-handler.js | 106 ++++++++++----- .../efile/templates/efile/expert_form.html | 26 ++-- efile_app/efile/templates/efile/upload.html | 17 ++- 7 files changed, 257 insertions(+), 105 deletions(-) diff --git a/efile_app/efile/settings_base.py b/efile_app/efile/settings_base.py index 2c2b2eb..0dc196a 100644 --- a/efile_app/efile/settings_base.py +++ b/efile_app/efile/settings_base.py @@ -44,6 +44,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "efile.middleware.NoCacheHTMLMiddleware", ] ROOT_URLCONF = "efile.urls" diff --git a/efile_app/efile/static/js/cascading-dropdowns.js b/efile_app/efile/static/js/cascading-dropdowns.js index 64f1c40..91548b3 100644 --- a/efile_app/efile/static/js/cascading-dropdowns.js +++ b/efile_app/efile/static/js/cascading-dropdowns.js @@ -177,7 +177,7 @@ class CascadingDropdowns { dropdown.disabled = false; if (response.data.length == 1 && dropdown.id == "party_type") { dropdown.parentElement.hidden = true; - dropdown.value = response.data[0].value || response.data[0].id; + this.selectPartyTypeRadio(dropdown, response.data[0].value || response.data[0].id); } } else { console.warn(`No data returned for ${fieldId}:`, response); @@ -745,6 +745,13 @@ class CascadingDropdowns { return; } + // Party type is rendered as a radio group (usually 2-3 options) rather + // than a select, since a dropdown is unnecessary friction for so few choices. + if (dropdown.id === 'party_type') { + this.populatePartyTypeRadios(dropdown, options); + return; + } + const placeholder = dropdown.querySelector('option[value=""]').textContent; // Clear dropdown and remove any visual selection indicators @@ -828,6 +835,59 @@ class CascadingDropdowns { } } + populatePartyTypeRadios(container, options) { + container.innerHTML = ""; + + if (!options || !Array.isArray(options) || options.length === 0) { + container.innerHTML = '

No party types available

'; + return; + } + + options.forEach((option, index) => { + const value = option.value || option.id; + const label = option.label || option.name || option.text; + + const wrapper = document.createElement("div"); + wrapper.className = "form-check"; + + const input = document.createElement("input"); + input.type = "radio"; + input.className = "form-check-input"; + input.name = "party_type"; + input.id = `party_type_${index}`; + input.value = value; + input.required = true; + + const radioLabel = document.createElement("label"); + radioLabel.className = "form-check-label"; + radioLabel.setAttribute("for", input.id); + radioLabel.textContent = label; + + wrapper.appendChild(input); + wrapper.appendChild(radioLabel); + container.appendChild(wrapper); + }); + + // Radios are recreated on every populate call, so attach the listener + // once on the (stable) container rather than per-input. + if (!container.dataset.changeListenerAttached) { + container.addEventListener("change", (e) => { + if (e.target && e.target.name === "party_type") { + this.selectedValues.party_type = e.target.value; + } + }); + container.dataset.changeListenerAttached = "true"; + } + } + + selectPartyTypeRadio(container, value) { + const radio = container.querySelector(`input[value="${value}"]`); + if (radio) { + radio.checked = true; + } + this.selectedValues.party_type = value; + } + showRecommendationNotice(dropdown, type) { // Remove any existing recommendation notice for this dropdown first const existingNotice = dropdown.parentNode.querySelector('.recommendation-notice'); @@ -951,6 +1011,13 @@ class CascadingDropdowns { return; } + // Party type is a radio group, not a select - reset it to its + // placeholder text instead of manipulating `; dropdown.disabled = false; } diff --git a/efile_app/efile/static/js/dynamic-form-sections.js b/efile_app/efile/static/js/dynamic-form-sections.js index b913fce..88b223f 100644 --- a/efile_app/efile/static/js/dynamic-form-sections.js +++ b/efile_app/efile/static/js/dynamic-form-sections.js @@ -899,18 +899,20 @@ class DynamicFormSections { break; case "party_type_dropdown": - // Create a dropdown for party types that will be populated via API + // Party type is a short, API-provided list (2-3 options in + // practice), so it's rendered as radio buttons rather than a + // dropdown. Populated later by loadPartyTypeDropdowns(). inputHtml = ` - + }"> +

Select Party Type

+ `; @@ -995,9 +997,6 @@ class DynamicFormSections { if (result.success && result.data) { const partyTypes = result.data; - // Clear existing options except the first one - dropdown.innerHTML = ''; - // If no saved data, try to find intelligent default from API response if (!defaultValue) { // Find the parent section title @@ -1037,20 +1036,7 @@ class DynamicFormSections { } } - // Add all party type options - let addedCount = 0; - partyTypes.forEach((partyType) => { - const option = document.createElement("option"); - option.value = partyType.code; - option.textContent = partyType.name; - // Set as selected if this is the default value - if (partyType.code === defaultValue) { - option.selected = true; - } - - dropdown.appendChild(option); - addedCount++; - }); + this.renderPartyTypeRadios(dropdown, partyTypes, defaultValue); } else { console.error( "Failed to load party types:", @@ -1058,16 +1044,14 @@ class DynamicFormSections { result ); - // Add error option dropdown.innerHTML = - ''; + '

Error loading party types

'; } } catch (error) { console.error(`Error loading party types for ${fieldId}:`, error); - // Add error option dropdown.innerHTML = - ''; + '

Error loading party types

'; } finally { // Hide loading spinner if (loadingSpinner) { @@ -1077,6 +1061,45 @@ class DynamicFormSections { }); } + renderPartyTypeRadios(container, partyTypes, defaultValue) { + container.innerHTML = ""; + + if (!partyTypes || partyTypes.length === 0) { + container.innerHTML = '

No party types available

'; + return; + } + + const groupName = container.dataset.fieldName || container.id; + const isRequired = container.dataset.required === "true"; + + partyTypes.forEach((partyType, index) => { + const wrapper = document.createElement("div"); + wrapper.className = "form-check"; + + const input = document.createElement("input"); + input.type = "radio"; + input.className = "form-check-input"; + input.name = groupName; + input.id = `${container.id}_${index}`; + input.value = partyType.code; + if (isRequired) { + input.required = true; + } + if (partyType.code === defaultValue) { + input.checked = true; + } + + const label = document.createElement("label"); + label.className = "form-check-label"; + label.setAttribute("for", input.id); + label.textContent = partyType.name; + + wrapper.appendChild(input); + wrapper.appendChild(label); + container.appendChild(wrapper); + }); + } + showDynamicSections() { if (this.dynamicSections) { this.dynamicSections.style.display = "block"; @@ -1119,6 +1142,9 @@ class DynamicFormSections { if (field) { if (field.type === "checkbox") { formData[fieldName] = field.checked; + } else if (field.type === "radio") { + const checked = document.querySelector(`input[name="${fieldName}"]:checked`); + formData[fieldName] = checked ? checked.value : ""; } else { formData[fieldName] = field.value; } @@ -1142,6 +1168,11 @@ class DynamicFormSections { if (field) { if (field.type === "checkbox") { field.checked = this.preservedFormData[fieldName]; + } else if (field.type === "radio") { + const radio = document.querySelector( + `input[name="${fieldName}"][value="${this.preservedFormData[fieldName]}"]` + ); + if (radio) radio.checked = true; } else { field.value = this.preservedFormData[fieldName]; } @@ -1202,35 +1233,33 @@ class DynamicFormSections { field.checked = Array.isArray(data[key]) ? data[key].includes(field.value) : data[key] === field.value; - } else if (field.classList.contains("party-type-dropdown")) { - // For party type dropdowns, we may need to wait for options to load - const setDropdownValue = () => { - // Check if the option exists - const option = field.querySelector( - `option[value="${data[key]}"]` + } else if (field.type === "radio") { + // Party type radios are added asynchronously once the + // API responds, so keep retrying until the matching + // option exists. + const setRadioValue = () => { + const radio = document.querySelector( + `input[name="${key}"][value="${data[key]}"]` ); - if (option) { - field.value = data[key]; - - // Add visual validation feedback after successful population - field.classList.remove("is-invalid"); - field.classList.add("is-valid"); - } else if (field.options.length <= 1) { - // Options haven't loaded yet, wait a bit longer - setTimeout(setDropdownValue, 500); + if (radio) { + radio.checked = true; + } else if ( + document.querySelectorAll(`input[name="${key}"]`).length === 0 + ) { + setTimeout(setRadioValue, 500); } }; - setDropdownValue(); + setRadioValue(); } else { field.value = data[key]; } fieldsPopulated++; - // Add visual validation feedback (but not for dropdowns until they're populated) + // Add visual validation feedback (but not for radios until they're populated) if ( + field.type !== "radio" && field.value && - field.value.trim() && - !field.classList.contains("party-type-dropdown") + field.value.trim() ) { field.classList.remove("is-invalid"); field.classList.add("is-valid"); diff --git a/efile_app/efile/static/js/form-validation.js b/efile_app/efile/static/js/form-validation.js index 1f282a9..2ef0341 100644 --- a/efile_app/efile/static/js/form-validation.js +++ b/efile_app/efile/static/js/form-validation.js @@ -505,9 +505,17 @@ class FormValidation { "document_type", ].includes(key) ) { - const field = this.form.querySelector(`[name="${key}"]`); - if (field) { - field.value = Array.isArray(data[key]) ? data[key][0] : data[key]; + const fields = this.form.querySelectorAll(`[name="${key}"]`); + if (fields.length === 1 && fields[0].type !== "radio") { + fields[0].value = Array.isArray(data[key]) ? data[key][0] : data[key]; + } else { + fields.forEach((field) => { + if (field.type === "radio" || field.type === "checkbox") { + field.checked = Array.isArray(data[key]) ? + data[key].includes(field.value) : + data[key] === field.value; + } + }); } } }); diff --git a/efile_app/efile/static/js/upload-handler.js b/efile_app/efile/static/js/upload-handler.js index 06a559f..ce0c6d5 100644 --- a/efile_app/efile/static/js/upload-handler.js +++ b/efile_app/efile/static/js/upload-handler.js @@ -309,7 +309,7 @@ class UploadHandler { await this.populateDocumentTypes(upload_data.lead_filing_type, document.getElementById("leadDocumentType")); } if (upload_data.lead_document_type) { - document.getElementById("leadDocumentType").value = upload_data.lead_document_type; + this.setRadioGroupValue(document.getElementById("leadDocumentType"), upload_data.lead_document_type); } if (upload_data.lead_cc_email) { let input_element = document.getElementById("leadCertifiedCopies") @@ -338,7 +338,7 @@ class UploadHandler { await this.populateDocumentTypes(d.filing_type, document.getElementById(`supportingDocumentType${index}`)); } if (d.document_type) { - document.getElementById(`supportingDocumentType${index}`).value = d.document_type; + this.setRadioGroupValue(document.getElementById(`supportingDocumentType${index}`), d.document_type); } if (d.cc_email) { let input_element = document.getElementById(`supportingFilingType${index}`); @@ -559,7 +559,7 @@ class UploadHandler { if (selectedFilingTypeId) { await this.populateDocumentTypes(selectedFilingTypeId, documentTypeSelect); } else { - documentTypeSelect.innerHTML = ''; + documentTypeSelect.innerHTML = '

Select filing type first

'; } }); } @@ -696,7 +696,7 @@ class UploadHandler { this.showError(`Please select a filing component for supporting document: ${this.uploadedFiles[i].name}`); return; } - const supportingDocumentType = document.getElementById(`supportingDocumentType${i}`)?.value; + const supportingDocumentType = this.getRadioGroupValue(document.getElementById(`supportingDocumentType${i}`)); if (!supportingDocumentType) { this.showError(`Please select a document type for supporting document: ${this.uploadedFiles[i].name}`); return; @@ -706,12 +706,12 @@ class UploadHandler { try { // Collect dropdown values for lead document const leadFilingTypeSelect = document.getElementById('leadFilingType'); - const leadDocumentTypeSelect = document.getElementById('leadDocumentType'); + const leadDocumentTypeContainer = document.getElementById('leadDocumentType'); const leadFilingType = leadFilingTypeSelect ? leadFilingTypeSelect.value : ''; const leadFilingTypeName = leadFilingTypeSelect && leadFilingTypeSelect.selectedOptions[0] ? leadFilingTypeSelect.selectedOptions[0].text : ''; - const leadDocumentType = leadDocumentTypeSelect ? leadDocumentTypeSelect.value : ''; - const leadDocumentTypeName = leadDocumentTypeSelect && leadDocumentTypeSelect.selectedOptions[0] ? leadDocumentTypeSelect.selectedOptions[0].text : ''; + const leadDocumentType = this.getRadioGroupValue(leadDocumentTypeContainer); + const leadDocumentTypeName = this.getRadioGroupText(leadDocumentTypeContainer); if (!leadFilingType || !leadDocumentType) { this.showError('Please select a filing type and document type for the lead document.'); @@ -732,9 +732,9 @@ class UploadHandler { supportingDropdowns.forEach((dropdown, index) => { const filingType = dropdown.value; const filingTypeName = dropdown.selectedOptions[0]?.text || ''; - const docTypeSelect = document.getElementById(`supportingDocumentType${index}`); - const docType = docTypeSelect?.value || ''; - const docTypeName = docTypeSelect?.selectedOptions[0]?.text || ''; + const docTypeContainer = document.getElementById(`supportingDocumentType${index}`); + const docType = this.getRadioGroupValue(docTypeContainer); + const docTypeName = this.getRadioGroupText(docTypeContainer); const component = this.globalFilingComponentSupport.id; const componentName = this.globalFilingComponentSupport.name; @@ -1009,15 +1009,15 @@ class UploadHandler { this.initializeFilingTypeDropdowns(); } - async populateDocumentTypes(filingTypeId, documentTypeSelect) { + async populateDocumentTypes(filingTypeId, documentTypeContainer) { try { // Get court data from Django context to pass required parameters const court = JSON.parse(document.getElementById("case-classification").textContent)["court"] || sessionStorage.getItem("selected_court"); if (!court) { console.error("Missing court parameter for document types API"); - documentTypeSelect.innerHTML = - ''; + documentTypeContainer.innerHTML = + '

Missing court data

'; return; } @@ -1028,27 +1028,71 @@ class UploadHandler { } const result = await apiUtils.get('/api/dropdowns/document-types', params, true); if (result.success && result.data) { - documentTypeSelect.innerHTML = - ''; - result.data.forEach((docType) => { - const option = document.createElement("option"); - option.value = docType.value || docType.code || docType.id; - option.textContent = - docType.text || docType.name || docType.description; - documentTypeSelect.appendChild(option); - }); + this.renderDocumentTypeRadios(documentTypeContainer, result.data); } else { console.error("API returned error:", result.error); - documentTypeSelect.innerHTML = - ''; + documentTypeContainer.innerHTML = + '

Error loading document types

'; } } catch (error) { console.error("Error loading document types:", error); - documentTypeSelect.innerHTML = - ''; + documentTypeContainer.innerHTML = + '

Error loading document types

'; } } + // Confidential/non-confidential is a binary choice (occasionally a third + // option), so it's rendered as radio buttons rather than a dropdown. + renderDocumentTypeRadios(container, options) { + container.innerHTML = ""; + const groupName = container.dataset.fieldName || container.id; + + options.forEach((docType, index) => { + const value = docType.value || docType.code || docType.id; + const text = docType.text || docType.name || docType.description; + + const wrapper = document.createElement("div"); + wrapper.className = "form-check"; + + const input = document.createElement("input"); + input.type = "radio"; + input.className = "form-check-input document-type-select"; + input.name = groupName; + input.id = `${container.id}_${index}`; + input.value = value; + input.required = true; + + const label = document.createElement("label"); + label.className = "form-check-label"; + label.setAttribute("for", input.id); + label.textContent = text; + + wrapper.appendChild(input); + wrapper.appendChild(label); + container.appendChild(wrapper); + }); + } + + getRadioGroupValue(container) { + if (!container) return ''; + const checked = container.querySelector('input[type="radio"]:checked'); + return checked ? checked.value : ''; + } + + getRadioGroupText(container) { + if (!container) return ''; + const checked = container.querySelector('input[type="radio"]:checked'); + if (!checked) return ''; + const label = container.querySelector(`label[for="${checked.id}"]`); + return label ? label.textContent.trim() : ''; + } + + setRadioGroupValue(container, value) { + if (!container) return; + const radio = container.querySelector(`input[type="radio"][value="${value}"]`); + if (radio) radio.checked = true; + } + setupCascadingDropdowns() { // Lead document cascading dropdowns const leadFilingTypeSelect = document.getElementById("leadFilingType"); @@ -1145,10 +1189,12 @@ function createSupportingDocumentOptions(index, fileName) {
- - +
+ Request Documents to be Sealed / Confidential? * +
+

Select filing type first

+
+
diff --git a/efile_app/efile/templates/efile/expert_form.html b/efile_app/efile/templates/efile/expert_form.html index b532727..690eca7 100644 --- a/efile_app/efile/templates/efile/expert_form.html +++ b/efile_app/efile/templates/efile/expert_form.html @@ -116,20 +116,18 @@

{% translate "Case type" %}

- - {% translate "This is your party type." %} - - +
+ + {% translate "Party Type" %}* + + {% translate "This is your party type." %} +
+

{% translate "Select a case type first" %}

+
+ +
diff --git a/efile_app/efile/templates/efile/upload.html b/efile_app/efile/templates/efile/upload.html index 3c67966..1fadc13 100644 --- a/efile_app/efile/templates/efile/upload.html +++ b/efile_app/efile/templates/efile/upload.html @@ -86,15 +86,14 @@

- - +
+ + {% translate "Request documents be sealed / confidential?" %}* + +
+

{% translate "Select filing type first" %}

+
+
From 48d08b3be22518ecee35cabf6dea715c57f77be1 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Thu, 23 Jul 2026 13:49:38 -0400 Subject: [PATCH 2/2] Add missing middleware file --- efile_app/efile/middleware.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 efile_app/efile/middleware.py diff --git a/efile_app/efile/middleware.py b/efile_app/efile/middleware.py new file mode 100644 index 0000000..15d338f --- /dev/null +++ b/efile_app/efile/middleware.py @@ -0,0 +1,25 @@ +class NoCacheHTMLMiddleware: + """Stop browsers from reusing a stale rendered page without asking the server first. + + Django's views send no Cache-Control at all, which leaves the browser free to + apply its own heuristic freshness and serve an old page byte-for-byte on a plain + reload -- no request even reaches the server. That's the same failure mode + WHITENOISE_MAX_AGE=0 (settings_dev.py) fixes for static JS/CSS, but it was still + open for the HTML itself: a template change (e.g. a