Enhance import preview functionality with editable fields and overlay logic for payload updates

This commit is contained in:
Ross
2025-10-20 22:35:01 +01:00
parent 8dac99b6d3
commit 45e50e9e93
2 changed files with 181 additions and 136 deletions
+167 -136
View File
@@ -2,9 +2,14 @@
{% csrf_token %}
<input type="hidden" name="session_key" value="{{ session_key }}">
{% for item in items %}
<div class="card mb-2">
<div class="card mb-2" id="import-item-{{ item.index }}">
<div class="card-body">
<h5 class="card-title">Question {{ item.index|add:1 }}</h5>
<div class="d-flex justify-content-between align-items-start">
<h5 class="card-title mb-0">Question {{ item.index|add:1 }}</h5>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary ms-2" data-item-index="{{ item.index }}" onclick="toggleEdit(this)">Edit</button>
</div>
</div>
{% if item.errors %}
<div class="alert alert-danger" role="alert">
<strong>Validation errors:</strong>
@@ -25,161 +30,168 @@
<strong>Note:</strong> Numeric references were removed from fields: <em>{{ item.stripped_fields|join:", " }}</em>
</div>
{% endif %}
{% if item.payload %}
<p><strong>Title:</strong> {{ item.payload.title|default:'(No title)'|safe }}</p>
<p><strong>Stem:</strong> {{ item.payload.stem|default:''|safe }}</p>
<p><strong>Answers:</strong>
<ul>
<li>A: {{ item.payload.a_answer|default:''|safe }}</li>
{% if item.payload.a_feedback %}
<p><strong>A Feedback:</strong> {{ item.payload.a_feedback|default:''|safe }}</p>
{% endif %}
<li>B: {{ item.payload.b_answer|default:''|safe }}</li>
{% if item.payload.b_feedback %}
<p><strong>B Feedback:</strong> {{ item.payload.b_feedback|default:''|safe }}</p>
{% endif %}
<li>C: {{ item.payload.c_answer|default:''|safe }}</li>
{% if item.payload.c_feedback %}
<p><strong>C Feedback:</strong> {{ item.payload.c_feedback|default:''|safe }}</p>
{% endif %}
<li>D: {{ item.payload.d_answer|default:''|safe }}</li>
{% if item.payload.d_feedback %}
<p><strong>D Feedback:</strong> {{ item.payload.d_feedback|default:''|safe }}</p>
{% endif %}
<li>E: {{ item.payload.e_answer|default:''|safe }}</li>
{% if item.payload.e_feedback %}
<p><strong>E Feedback:</strong> {{ item.payload.e_feedback|default:''|safe }}</p>
{% endif %}
</ul>
<p><strong>Best Answer:</strong> {{ item.payload.best_answer|default:'' }}
</p>
<p><strong>Feedback:</strong> {{ item.payload.feedback|default:''|safe }}</p>
<p><strong>Sources:</strong>
{% if item.payload.sources %}
<ul>
{% for source in item.payload.sources %}
<li>{{ source }}</li>
{% endfor %}
</ul>
{% else %}
None
{% endif %}
</p>
{% endif %}
<p><strong>M2M fields (click to exclude)</strong></p>
<p class="small text-muted">Green = will be created/attached. Click a value to exclude it (grey).</p>
<div class="mb-2">
<label class="form-label"><strong>Title</strong></label>
<input class="form-control form-control-sm" name="payload_{{ item.index }}_title" value="{{ item.payload.title|default_if_none:'' }}" disabled>
</div>
<div class="mb-2">
<label class="form-label"><strong>Stem</strong></label>
<textarea class="form-control form-control-sm" name="payload_{{ item.index }}_stem" rows="3" disabled>{{ item.payload.stem|default_if_none:'' }}</textarea>
</div>
<div class="mb-2">
<label class="form-label"><strong>Answers</strong></label>
<div class="mb-1">
<label class="form-label small">A</label>
<input class="form-control form-control-sm" name="payload_{{ item.index }}_a_answer" value="{{ item.payload.a_answer|default_if_none:'' }}" disabled>
<label class="form-label small mt-1">A Feedback</label>
<textarea class="form-control form-control-sm" name="payload_{{ item.index }}_a_feedback" rows="2" disabled>{{ item.payload.a_feedback|default_if_none:'' }}</textarea>
</div>
<div class="mb-1">
<label class="form-label small">B</label>
<input class="form-control form-control-sm" name="payload_{{ item.index }}_b_answer" value="{{ item.payload.b_answer|default_if_none:'' }}" disabled>
<label class="form-label small mt-1">B Feedback</label>
<textarea class="form-control form-control-sm" name="payload_{{ item.index }}_b_feedback" rows="2" disabled>{{ item.payload.b_feedback|default_if_none:'' }}</textarea>
</div>
<div class="mb-1">
<label class="form-label small">C</label>
<input class="form-control form-control-sm" name="payload_{{ item.index }}_c_answer" value="{{ item.payload.c_answer|default_if_none:'' }}" disabled>
<label class="form-label small mt-1">C Feedback</label>
<textarea class="form-control form-control-sm" name="payload_{{ item.index }}_c_feedback" rows="2" disabled>{{ item.payload.c_feedback|default_if_none:'' }}</textarea>
</div>
<div class="mb-1">
<label class="form-label small">D</label>
<input class="form-control form-control-sm" name="payload_{{ item.index }}_d_answer" value="{{ item.payload.d_answer|default_if_none:'' }}" disabled>
<label class="form-label small mt-1">D Feedback</label>
<textarea class="form-control form-control-sm" name="payload_{{ item.index }}_d_feedback" rows="2" disabled>{{ item.payload.d_feedback|default_if_none:'' }}</textarea>
</div>
<div class="mb-1">
<label class="form-label small">E</label>
<input class="form-control form-control-sm" name="payload_{{ item.index }}_e_answer" value="{{ item.payload.e_answer|default_if_none:'' }}" disabled>
<label class="form-label small mt-1">E Feedback</label>
<textarea class="form-control form-control-sm" name="payload_{{ item.index }}_e_feedback" rows="2" disabled>{{ item.payload.e_feedback|default_if_none:'' }}</textarea>
</div>
<div class="mb-1">
<label class="form-label small">Best Answer</label>
<input class="form-control form-control-sm" name="payload_{{ item.index }}_best_answer" value="{{ item.payload.best_answer|default_if_none:'' }}" disabled>
</div>
<div class="mb-1">
<label class="form-label small">Feedback</label>
<textarea class="form-control form-control-sm" name="payload_{{ item.index }}_feedback" rows="2" disabled>{{ item.payload.feedback|default_if_none:'' }}</textarea>
</div>
<div class="mb-1 small text-muted">Sources: {% if item.payload.sources %}{{ item.payload.sources|join:", " }}{% else %}None{% endif %}</div>
<p><strong>M2M fields (click to exclude)</strong></p>
<p class="small text-muted">Green = will be created/attached. Click a value to exclude it (grey).</p>
<div class="mb-2">
<!-- Finding -->
<div class="m2m-group" data-model="Finding"><strong>Finding:</strong>
{% if item.resolved_m2m.finding %}
{% for val in item.resolved_m2m.finding %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Finding" data-value="{{ val }}">{{ val }}</button>
<div class="m2m-group" data-model="Finding"><strong>Finding:</strong>
{% if item.resolved_m2m.finding %}
{% for val in item.resolved_m2m.finding %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Finding" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
{% if item.missing_map.finding %}
{% for val in item.missing_map.finding %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Finding" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
<span class="text-muted m2m-none" style="display:none">(none)</span>
{% if item.missing_map.finding %}
<div class="small text-info">Would create: {{ item.missing_map.finding|join:", " }}</div>
{% endif %}
</div>
</div>
<!-- Structure -->
<div class="m2m-group" data-model="Structure"><strong>Structure:</strong>
{% if item.resolved_m2m.structure %}
{% for val in item.resolved_m2m.structure %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Structure" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
{% if item.missing_map.finding %}
{% for val in item.missing_map.finding %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Finding" data-value="{{ val }}">{{ val }}</button>
{% if item.missing_map.structure %}
{% for val in item.missing_map.structure %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Structure" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
<span class="text-muted m2m-none" style="display:none">(none)</span>
{% if item.missing_map.finding %}
<div class="small text-info">Would create: {{ item.missing_map.finding|join:", " }}</div>
{% if item.missing_map.structure %}
<div class="small text-info">Would create: {{ item.missing_map.structure|join:", " }}</div>
{% endif %}
</div>
</div>
<!-- Structure -->
<div class="m2m-group" data-model="Structure"><strong>Structure:</strong>
{% if item.resolved_m2m.structure %}
{% for val in item.resolved_m2m.structure %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Structure" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
{% if item.missing_map.structure %}
{% for val in item.missing_map.structure %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Structure" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
<span class="text-muted m2m-none" style="display:none">(none)</span>
{% if item.missing_map.structure %}
<div class="small text-info">Would create: {{ item.missing_map.structure|join:", " }}</div>
{% endif %}
</div>
<!-- Condition -->
<div class="m2m-group" data-model="Condition"><strong>Condition:</strong>
{% if item.resolved_m2m.condition %}
{% for val in item.resolved_m2m.condition %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Condition" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
{% if item.missing_map.condition %}
{% for val in item.missing_map.condition %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Condition" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
<span class="text-muted m2m-none" style="display:none">(none)</span>
{% if item.missing_map.condition %}
<div class="small text-info">Would create: {{ item.missing_map.condition|join:", " }}</div>
{% endif %}
</div>
<div class="m2m-group" data-model="Condition"><strong>Condition:</strong>
{% if item.resolved_m2m.condition %}
{% for val in item.resolved_m2m.condition %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Condition" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
{% if item.missing_map.condition %}
{% for val in item.missing_map.condition %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Condition" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
<span class="text-muted m2m-none" style="display:none">(none)</span>
{% if item.missing_map.condition %}
<div class="small text-info">Would create: {{ item.missing_map.condition|join:", " }}</div>
{% endif %}
</div>
<!-- Presentation -->
<div class="m2m-group" data-model="Presentation"><strong>Presentation:</strong>
{% if item.resolved_m2m.presentation %}
{% for val in item.resolved_m2m.presentation %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Presentation" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
{% if item.missing_map.presentation %}
{% for val in item.missing_map.presentation %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Presentation" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
<span class="text-muted m2m-none" style="display:none">(none)</span>
{% if item.missing_map.presentation %}
<div class="small text-info">Would create: {{ item.missing_map.presentation|join:", " }}</div>
{% endif %}
</div>
<div class="m2m-group" data-model="Presentation"><strong>Presentation:</strong>
{% if item.resolved_m2m.presentation %}
{% for val in item.resolved_m2m.presentation %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Presentation" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
{% if item.missing_map.presentation %}
{% for val in item.missing_map.presentation %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Presentation" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
<span class="text-muted m2m-none" style="display:none">(none)</span>
{% if item.missing_map.presentation %}
<div class="small text-info">Would create: {{ item.missing_map.presentation|join:", " }}</div>
{% endif %}
</div>
<!-- Subspecialty -->
<div class="m2m-group" data-model="Subspecialty"><strong>Subspecialty:</strong>
{% if item.resolved_m2m.subspecialty %}
{% for val in item.resolved_m2m.subspecialty %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Subspecialty" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
{% if item.missing_map.subspecialty %}
{% for val in item.missing_map.subspecialty %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Subspecialty" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
<span class="text-muted m2m-none" style="display:none">(none)</span>
{% if item.missing_map.subspecialty %}
<div class="small text-info">Would create: {{ item.missing_map.subspecialty|join:", " }}</div>
{% endif %}
</div>
<! -- Category -->
<div class="m2m-group" data-model="Category"><strong>Category:</strong>
{{ item.payload.category|default:'(none)' }}
<div class="m2m-group" data-model="Subspecialty"><strong>Subspecialty:</strong>
{% if item.resolved_m2m.subspecialty %}
{% for val in item.resolved_m2m.subspecialty %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Subspecialty" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
{% if item.missing_map.subspecialty %}
{% for val in item.missing_map.subspecialty %}
<button type="button" class="m2m-toggle btn btn-sm btn-success ms-1" data-idx="{{ item.index }}" data-model="Subspecialty" data-value="{{ val }}">{{ val }}</button>
{% endfor %}
{% endif %}
<span class="text-muted m2m-none" style="display:none">(none)</span>
{% if item.missing_map.subspecialty %}
<div class="small text-info">Would create: {{ item.missing_map.subspecialty|join:", " }}</div>
{% endif %}
</div>
<! -- Category -->
<div class="m2m-group" data-model="Category"><strong>Category:</strong>
{{ item.payload.category|default:'(none)' }}
</div>
</div>
<!-- DEBUG: show raw resolved and missing maps for troubleshooting -->
<div class="card-footer text-monospace small">
<details>
<summary>Debug: resolved_m2m / missing_map (click to expand)</summary>
<div class="card-footer text-monospace small">
<details>
<summary>Debug: resolved_m2m / missing_map (click to expand)</summary>
<pre style="white-space:pre-wrap;">resolved_m2m: {{ item.resolved_m2m|default:"{}" }}
missing_map: {{ item.missing_map|default:"{}" }}</pre>
</details>
</div>
<div>
<label><input type="checkbox" name="selected" value="{{ item.index }}" checked> Include</label>
</div>
</details>
</div>
<div>
<label><input type="checkbox" name="selected" value="{{ item.index }}" checked> Include</label>
</div>
<!-- container for hidden exclude inputs for this item -->
<div id="exclude-container-{{ item.index }}"></div>
<div id="exclude-container-{{ item.index }}"></div>
</div>
</div>
</div>
{% endfor %}
<div class="d-flex gap-2">
@@ -255,3 +267,22 @@ missing_map: {{ item.missing_map|default:"{}" }}</pre>
</script>
<!-- Bottom controls removed: submit and cancel are inside the form above -->
<script>
// Toggle editing of payload inputs for a specific item (disabled by default)
function toggleEdit(btn){
const idx = btn.getAttribute('data-item-index');
if(!idx) return;
const card = document.getElementById('import-item-' + idx);
if(!card) return;
const selector = 'input[name^="payload_' + idx + '_"], textarea[name^="payload_' + idx + '_"]';
const inputs = card.querySelectorAll(selector);
if(inputs.length === 0) return;
// check current state from first input
const currentlyDisabled = inputs[0].disabled === true;
// toggle
inputs.forEach(function(i){ i.disabled = currentlyDisabled ? false : true; });
// update button text
btn.textContent = currentlyDisabled ? 'Lock' : 'Edit';
}
</script>
+14
View File
@@ -950,6 +950,20 @@ def import_llm_confirm(request):
except Exception:
errors.append({"index": idx, "errors": ["Missing candidate"]})
continue
# Overlay any edited fields posted from the preview form (names like payload_<idx>_stem)
overlay = {}
prefix = f"payload_{idx}_"
for k, v in request.POST.items():
if k.startswith(prefix):
field = k[len(prefix):]
overlay[field] = v
if overlay:
# apply overlay onto a shallow copy
new_payload = dict(payload)
for fk, fv in overlay.items():
# convert list-like fields and numeric conversions are not performed here; keep simple strings/lists
new_payload[fk] = fv
payload = new_payload
# Strip numeric bracket references here as well, before confirm import
payload, stripped = _strip_numeric_refs_in_payload(payload)
if stripped: