Skip to content

Refactor: Choice Answer#286

Open
lebalz wants to merge 37 commits into
feature/choice-answerfrom
refactor-attempt/choice-answer
Open

Refactor: Choice Answer#286
lebalz wants to merge 37 commits into
feature/choice-answerfrom
refactor-attempt/choice-answer

Conversation

@lebalz
Copy link
Copy Markdown
Contributor

@lebalz lebalz commented Apr 6, 2026

Plan

  • ChoiceAnswers always have either an explicite id (uuid) when standalone or a qid when inside Quiz
  • Create documents for each Quiz-Item:
    • having a prop qid when inside a Quiz (to ensure ordering)
    • attach to the Quiz' DocumentRoot or to it's own DocumentRoot when standalone
  • create an interface iAssessable providing all needed methods to assess quiz or answers

@lebalz lebalz changed the title move ChoiceAnswer model to folder WIP: refactor(attempt): Choice Answer Apr 21, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 21, 2026

Deploy Preview for teaching-dev ready!

Name Link
🔨 Latest commit 3a09b8c
🔍 Latest deploy log https://app.netlify.com/projects/teaching-dev/deploys/6a1ed44f80c9ea000855ef32
😎 Deploy Preview https://deploy-preview-286--teaching-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

…ed components

Co-authored-by: Copilot <copilot@github.com>
Copy link
Copy Markdown
Contributor Author

@lebalz lebalz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current step: impement iAssessable to ChoiceAnswer and after that implement BooleanAnswer

lebalz

This comment was marked as duplicate.

@lebalz lebalz force-pushed the refactor-attempt/choice-answer branch from 7e375fa to 2ae412a Compare May 27, 2026 16:54
@lebalz lebalz changed the title WIP: refactor(attempt): Choice Answer Refactor: Choice Answer May 31, 2026
@lebalz
Copy link
Copy Markdown
Contributor Author

lebalz commented Jun 1, 2026

Ready for Review @SilasBerger :)

Current target is github.com/GBSL-Informatik/teaching-dev/tree/feature/choice-answer but we might want to directly merge to main? not sure how to proceed and what would be easier for review.

@lebalz lebalz requested a review from SilasBerger June 1, 2026 08:09
};

class PageReadChecker extends iDocument<'page_read_check'> implements iTaskableDocument<'page_read_check'> {
readonly hideFromOverview = false;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a default value — implementing models, e.g. ChoiceAnswer can use this flag to hide themselves from the Taskable Overview when inside a quiz.

Comment thread src/api/document.ts
Comment on lines +48 to +50
interface AssessableData {
assessed: boolean;
qid?: string;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic payload for all answer types which can be assessed

Comment on lines +59 to +63
<QuestionCard doc={doc}>
<DocContext.Provider value={doc}>{props.children}</DocContext.Provider>
</QuestionCard>
);
}) as React.FC<ChoiceAnswerProps> & ChoiceAnswerSubComponents;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the refactor, this is all that is needed for the Choice Answer :)

Comment on lines +15 to +16
// !! must be a stable reference, otherwise the whole list will re-render on every change
onChange: (doc: AssessableTypeModelMapping[T], optionIndex: number, checked: boolean) => void;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!Important - either define the function outside the component or wrap it in a React.useCallback

@@ -0,0 +1,60 @@
.optionsBlock {
--tdev-assessable-btn-remove-answer-transition-duration: 0.15s;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer using classic, tdev-scoped css variables (--tdev...) instead of scss variables ($tdevAssess...)

}}
header={
<>
<h3 className={clsx(styles.questionTitle)}>{doc.displayTitle}</h3>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when calculating frontend-stuff as @computed inside the model, the component will only rerender, when the value changes. By deault, frontend specific stuff is prefixed with display.

)}
ref={ref}
>
<DocumentRootIdContext id={props.id}>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only the id is provided as context, the rest comes from the store...

const TrueFalseAnswer = observer((props: Props) => {
const [meta] = React.useState(new ModelMeta({ ...props }));
const docRootId = useDocumentRootId(props.id);
const doc = useFirstDocumentBy(docRootId, meta, props.qid);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

questions inside a quiz have a qid and are attached to the same documentRoot as the quiz. In case a qid is present, we are in a quiz and return the document based of the qid. Otherwise the mainDocument of the documentRoot is returned.

Comment on lines +7 to +14
const useLinkedMetaModel = <Type extends AssessableType>(
doc: AssessableTypeModelMapping[Type] | undefined | null,
meta: AssessableMeta<Type>
) => {
React.useEffect(() => {
(doc as iAssessable<Type> | undefined)?.setLinkedMeta(meta);
}, [doc, meta]);
};
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meta is normally attached to the documentRoot and is shared by all attached documents. Since not all documents share the same type, the concept of linkedMeta is introduced, which attaches the model meta to the document when it gets rendered. Like that, specific props (as the scoring function) are accessable from inside the model and are not shared between the instances.

Comment on lines +175 to +194
@computed
get maxHits(): number {
return this.linkedMeta?.correct?.length || 0;
}

/**
* Returns the number of correctly responded items.
* This can be "correct choices" for MC questions, "correct matched words" for texts or simply "1/0" for single-choice questions.
*/
get hits(): number {
return 0;
}

/**
* Returns the number of incorrectly responded items.
* This can be "incorrect choices" for MC questions, "wrong matched words" in a text or simply "0/1" for single-choice questions.
*/
get misses(): number {
return 0;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simple heuristic without a scoring function. Might not be correct for all question types, e.g. SingleChoiceAnswer with multiple correct Options would have too much maxHits.

quizNode.attributes.push(qids);
};

const DEFAULT_OPTIONS: Options = {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make the answer components configurable to enable hassle-free extension

Comment on lines +226 to +237
},
{
name: 'Quiz',
docTypeExtractor: () => 'quiz'
},
{
name: 'ChoiceAnswer',
docTypeExtractor: () => 'choice_answer'
},
{
name: 'TrueFalseAnswer',
docTypeExtractor: () => 'true_false_answer'
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed for the task overview

if (typesSet.size === 1) {
return types[0];
}
if (typesSet.has('quiz')) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whenever we find a quiz-document along other doc types (e.g. choiceAnswer/TrueFalseAnswer), we return quiz, since all are attached to the same doc root, but only the quiz is relevant for the root document

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant