Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions efile_app/efile/middleware.py
Original file line number Diff line number Diff line change
@@ -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 <select> replaced with radio
inputs) could sit invisible until the user did a hard refresh.

Runs after WhiteNoise in MIDDLEWARE, so it never touches the responses WhiteNoise
already serves (and already marks with its own Cache-Control) -- only the actual
Django view responses, which is exactly what has no header set today. These pages
also carry per-session case data, so not caching them is correct beyond just
dev convenience.
"""

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
response = self.get_response(request)
if "Cache-Control" not in response:
response["Cache-Control"] = "no-store"
return response
1 change: 1 addition & 0 deletions efile_app/efile/settings_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
73 changes: 72 additions & 1 deletion efile_app/efile/static/js/cascading-dropdowns.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -828,6 +835,59 @@ class CascadingDropdowns {
}
}

populatePartyTypeRadios(container, options) {
container.innerHTML = "";

if (!options || !Array.isArray(options) || options.length === 0) {
container.innerHTML = '<p class="text-muted mb-0">No party types available</p>';
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');
Expand Down Expand Up @@ -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 <option> elements.
if (dropdown.id === 'party_type') {
dropdown.innerHTML = '<p class="text-muted mb-0">Select a case type first</p>';
return;
}

let placeholder =
dropdown.querySelector('option[value=""]')?.textContent ||
"Please select...";
Expand Down Expand Up @@ -984,6 +1051,10 @@ class CascadingDropdowns {
}

showError(dropdown, message) {
if (dropdown.id === 'party_type') {
dropdown.innerHTML = `<p class="text-danger mb-0">Error: ${message}</p>`;
return;
}
dropdown.innerHTML = `<option value="">Error: ${message}</option>`;
dropdown.disabled = false;
}
Expand Down
125 changes: 77 additions & 48 deletions efile_app/efile/static/js/dynamic-form-sections.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 class="form-select party-type-dropdown"
id="${fieldId}"
name="${fieldName}"
<div class="party-type-dropdown"
id="${fieldId}"
data-field-name="${fieldName}"
data-required="${field.required ? "true" : "false"}"
data-api-endpoint="${
field.api_endpoint ||
"/api/dropdowns/party-types/"
}"
${requiredAttr}>
<option value="">Select Party Type</option>
</select>
}">
<p class="text-muted mb-0">Select Party Type</p>
</div>
<div class="loading-spinner" id="loading-${fieldId}" style="display: none;">
<i class="fas fa-spinner fa-spin"></i> Loading party types...
</div>`;
Expand Down Expand Up @@ -995,9 +997,6 @@ class DynamicFormSections {
if (result.success && result.data) {
const partyTypes = result.data;

// Clear existing options except the first one
dropdown.innerHTML = '<option value="">Select Party Type</option>';

// If no saved data, try to find intelligent default from API response
if (!defaultValue) {
// Find the parent section title
Expand Down Expand Up @@ -1037,37 +1036,22 @@ 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:",
result.error || "Unknown error",
result
);

// Add error option
dropdown.innerHTML =
'<option value="">Error loading party types</option>';
'<p class="text-danger mb-0">Error loading party types</p>';
}
} catch (error) {
console.error(`Error loading party types for ${fieldId}:`, error);

// Add error option
dropdown.innerHTML =
'<option value="">Error loading party types</option>';
'<p class="text-danger mb-0">Error loading party types</p>';
} finally {
// Hide loading spinner
if (loadingSpinner) {
Expand All @@ -1077,6 +1061,45 @@ class DynamicFormSections {
});
}

renderPartyTypeRadios(container, partyTypes, defaultValue) {
container.innerHTML = "";

if (!partyTypes || partyTypes.length === 0) {
container.innerHTML = '<p class="text-muted mb-0">No party types available</p>';
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";
Expand Down Expand Up @@ -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;
}
Expand All @@ -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];
}
Expand Down Expand Up @@ -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");
Expand Down
14 changes: 11 additions & 3 deletions efile_app/efile/static/js/form-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
}
}
});
Expand Down
Loading