All files / src/app/teacher-page/page/assignments/data-analytics/item-analysis itemAnalysisTransform.js

55.88% Statements 19/34
44.68% Branches 21/47
66.66% Functions 4/6
51.72% Lines 15/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120                                                                  31x   31x 31x                                                                                     56x   29x 29x   24x 24x   23x                 11x 13x 13x 13x 52x     13x                                
/**
 * Pure transforms for the teacher Item Analysis table.
 *
 * Extracted from AnalysisTable.jsx so the response→row mapping can be unit
 * tested without pulling in MaterialReactTable.
 *
 * Backend response shape (per question):
 *   {
 *     question_type, category, student_expectation, correct_answer, points,
 *     total_correct, total_incorrect,            // percentage strings, e.g. "100.0%"
 *     student_answers: [{ letter, text, total }] // total = % of students who picked it
 *   }
 *
 * Developer: Allan Ninal
 */
 
/**
 * Extracts a display-ready HTML string from a choice/answer entry: an
 * erudition-math editor document ({ blocks: [...] }), a JSON-encoded blocks
 * document ({ text: "..." }), or a plain HTML/text string.
 *
 * Math-editor formulas are stored inline as pre-rendered
 * `<span class="mfe-formula" data-latex="…">…</span>` markup (MathLive's
 * static output, produced by src/lib/tinymce/plugins/math-formula/plugin.js).
 * That markup is returned as-is — the caller is responsible for sanitizing it
 * with sanitizeRichText before dangerouslySetInnerHTML — so the table can
 * render an actual rendered formula instead of collapsing it to its raw LaTeX
 * source text.
 *
 * @param {Object|string} content
 * @returns {string} HTML (or plain text) content, or "NA" when empty/unparseable
 */
export function extractContentHtml(content) {
    Iif (!content) return "NA";
 
    Eif (typeof content === "string") {
        return content.trim() || "NA";
    }
 
    if (typeof content === "object") {
        if (Array.isArray(content.blocks)) {
            const html = content.blocks
                .map((block) => block?.data?.text || "")
                .join(" ")
                .trim();
            return html || "NA";
        }
 
        if (typeof content.text === "string") {
            try {
                const parsed = JSON.parse(content.text);
                if (parsed && Array.isArray(parsed.blocks)) {
                    const html = parsed.blocks
                        .map((block) => block?.data?.text || "")
                        .join(" ")
                        .trim();
                    return html || "NA";
                }
            } catch {
                // Not JSON — fall through to treating it as raw HTML/text below.
            }
            return content.text.trim() || "NA";
        }
    }
 
    return "NA";
}
 
/**
 * Builds a single answer-choice cell's HTML, combining the choice content
 * with the percentage of students who selected it, e.g. "6 — 100.0%" (or the
 * choice's rendered math markup followed by the same suffix).
 *
 * Returns "NA" for non-applicable choices (e.g. free-response placeholders,
 * where the backend sends a {letter:"NA", text:"NA"} slot).
 * @param {{letter?: string, text?: string|Object, total?: string|number}} option
 * @returns {string} HTML string — sanitize before rendering
 */
export function formatChoiceCell(option) {
    if (!option) return "NA";
 
    const html = extractContentHtml(option.text);
    if (html === "NA") return "NA";
 
    const total = option.total;
    if (total === undefined || total === null || total === "") return html;
 
    return `${html} — ${total}`;
}
 
/**
 * Maps the item-analysis API response into table rows.
 * @param {{item_analysis: Array}} entry
 * @returns {Array<Object>}
 */
export function transformData(entry) {
    return (entry?.item_analysis || []).map((item, idx) => {
        const answers = item.student_answers || [];
        const answerCells = [];
        for (let i = 0; i < 4; i++) {
            answerCells[i] = formatChoiceCell(answers[i]);
        }
 
        return {
            question_number: (idx + 1).toString(),
            question_type: item.question_type,
            category: item.category || "NA",
            student_expectations: item.student_expectation || "NA",
            answer_choiceA: answerCells[0],
            answer_choiceB: answerCells[1],
            answer_choiceC: answerCells[2],
            answer_choiceD: answerCells[3],
            correct_answer: item.correct_answer || "NA",
            points: item.points || "NA",
            percent_correct: item.total_correct,
            percent_incorrect: item.total_incorrect,
        };
    });
}