Merge fx-8-response-area-attach
This commit is contained in:
commit
e6a2480818
@ -92,4 +92,28 @@ describe('exam setup canvas serialization', () => {
|
|||||||
expect(payload.boundaries.find((b) => b.id === '44444444-4444-4444-8444-444444444444')).toMatchObject({ source: 'ai', confirmed: false, confidence: 0.62, derivation: 'g6' })
|
expect(payload.boundaries.find((b) => b.id === '44444444-4444-4444-8444-444444444444')).toMatchObject({ source: 'ai', confirmed: false, confidence: 0.62, derivation: 'g6' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('never drops a response region that has no enclosing part — it falls back to a question', () => {
|
||||||
|
const payload = serializeCanvasShapes(template, [
|
||||||
|
{ id: 'b-top', kind: 'boundary', x: 40, y: 100, w: 700, h: 8, label: 'Q1 start' },
|
||||||
|
{ id: 'b-bottom', kind: 'boundary', x: 40, y: 700, w: 700, h: 8, label: 'Q1 end' },
|
||||||
|
{ id: 'part-1', kind: 'part', x: 100, y: 180, w: 400, h: 120, label: 'Q1(a)', maxMarks: 3 },
|
||||||
|
{ id: 'resp-far', kind: 'response', x: 100, y: 520, w: 300, h: 90, responseForm: 'lines' }, // outside the part box
|
||||||
|
])
|
||||||
|
const part = payload.questions.find((q) => !q.is_container)
|
||||||
|
expect(payload.response_areas).toHaveLength(1) // previously dropped (no containing part)
|
||||||
|
expect(payload.response_areas[0].question_id).toBe(part?.id) // falls back to the part, not lost
|
||||||
|
})
|
||||||
|
|
||||||
|
it('respects a region persisted question over geometric nearest when no part contains it', () => {
|
||||||
|
const A = '11111111-1111-4111-8111-111111111111'
|
||||||
|
const B = '22222222-2222-4222-8222-222222222222'
|
||||||
|
const R = '33333333-3333-4333-8333-333333333333'
|
||||||
|
const payload = serializeCanvasShapes(template, [
|
||||||
|
{ id: 'pa', kind: 'part', x: 100, y: 120, w: 300, h: 80, label: 'A', questionId: A },
|
||||||
|
{ id: 'pb', kind: 'part', x: 100, y: 500, w: 300, h: 80, label: 'B', questionId: B },
|
||||||
|
{ id: R, kind: 'response', x: 120, y: 260, w: 200, h: 60, questionId: B }, // contained by neither; nearest is A
|
||||||
|
])
|
||||||
|
expect(payload.response_areas.find((r) => r.id === R)?.question_id).toBe(B) // persisted B wins over nearest A
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -147,11 +147,22 @@ export function serializeCanvasShapes(template: ExamTemplateDetail, shapes: Exam
|
|||||||
questions.push({ id: qid, parent_id: parentBand?.questionId ?? null, label: part.label || `Part ${index + 1}`, order: index, max_marks: Number(part.maxMarks ?? 0), answer_type: part.answerType ?? 'written', mcq_options: null, mark_scheme: {}, is_container: false, spec_ref: null, bounds: bounds(part), page: pageForShape(part, pages), source: persistedSource(part), confirmed: persistedConfirmed(part), confidence: persistedConfidence(part), derivation: persistedDerivation(part) })
|
questions.push({ id: qid, parent_id: parentBand?.questionId ?? null, label: part.label || `Part ${index + 1}`, order: index, max_marks: Number(part.maxMarks ?? 0), answer_type: part.answerType ?? 'written', mcq_options: null, mark_scheme: {}, is_container: false, spec_ref: null, bounds: bounds(part), page: pageForShape(part, pages), source: persistedSource(part), confirmed: persistedConfirmed(part), confidence: persistedConfidence(part), derivation: persistedDerivation(part) })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Resolve each region's owner question. Order: current geometric containment (a user who drags a
|
||||||
|
// region into a part re-attaches it) → the PERSISTED attachment if it still points at a saved
|
||||||
|
// question (survives pixel-overflow drift and regions with no enclosing part) → nearest part on the
|
||||||
|
// page → first part → first question of any kind. exam_response_areas.question_id is NOT NULL, so we
|
||||||
|
// never silently drop a region while any question exists (only a template with zero questions can).
|
||||||
|
const questionIds = new Set(questions.map((q) => q.id))
|
||||||
const response_areas: TemplateReplacePayload['response_areas'] = []
|
const response_areas: TemplateReplacePayload['response_areas'] = []
|
||||||
for (const region of regions) {
|
for (const region of regions) {
|
||||||
const containingPart = parts.find((part) => contains(bounds(part), bounds(region)))
|
const containingPart = parts.find((part) => contains(bounds(part), bounds(region)))
|
||||||
const fallbackPart = parts.find((part) => pageForShape(part, pages) === pageForShape(region, pages)) ?? parts[0]
|
const persisted = isUuid(region.questionId) && questionIds.has(region.questionId) ? region.questionId : undefined
|
||||||
const questionId = containingPart ? partQuestionIds.get(containingPart.id) : fallbackPart ? partQuestionIds.get(fallbackPart.id) : undefined
|
const nearestPart = parts.find((part) => pageForShape(part, pages) === pageForShape(region, pages)) ?? parts[0]
|
||||||
|
const questionId =
|
||||||
|
(containingPart && partQuestionIds.get(containingPart.id))
|
||||||
|
|| persisted
|
||||||
|
|| (nearestPart && partQuestionIds.get(nearestPart.id))
|
||||||
|
|| questions[0]?.id
|
||||||
if (!questionId) continue
|
if (!questionId) continue
|
||||||
const kind = region.kind as ExamCanvasRegionKind
|
const kind = region.kind as ExamCanvasRegionKind
|
||||||
response_areas.push({ id: isUuid(region.id) ? region.id : newDomainId(), question_id: questionId, page: pageForShape(region, pages), bounds: bounds(region), kind, response_form: kind === 'response' ? (region.responseForm ?? 'lines') : null, context_type: kind === 'context' ? (region.contextType ?? 'generic') : null, source: persistedSource(region), confirmed: persistedConfirmed(region), confidence: persistedConfidence(region), mark_subtype: null, derivation: persistedDerivation(region) })
|
response_areas.push({ id: isUuid(region.id) ? region.id : newDomainId(), question_id: questionId, page: pageForShape(region, pages), bounds: bounds(region), kind, response_form: kind === 'response' ? (region.responseForm ?? 'lines') : null, context_type: kind === 'context' ? (region.contextType ?? 'generic') : null, source: persistedSource(region), confirmed: persistedConfirmed(region), confidence: persistedConfidence(region), mark_subtype: null, derivation: persistedDerivation(region) })
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user