Refactor: Choice Answer#286
Conversation
✅ Deploy Preview for teaching-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…ed components Co-authored-by: Copilot <copilot@github.com>
lebalz
left a comment
There was a problem hiding this comment.
current step: impement iAssessable to ChoiceAnswer and after that implement BooleanAnswer
7e375fa to
2ae412a
Compare
…faces in ChoiceAnswer
|
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. |
| }; | ||
|
|
||
| class PageReadChecker extends iDocument<'page_read_check'> implements iTaskableDocument<'page_read_check'> { | ||
| readonly hideFromOverview = false; |
There was a problem hiding this comment.
Adds a default value — implementing models, e.g. ChoiceAnswer can use this flag to hide themselves from the Taskable Overview when inside a quiz.
| interface AssessableData { | ||
| assessed: boolean; | ||
| qid?: string; |
There was a problem hiding this comment.
Generic payload for all answer types which can be assessed
| <QuestionCard doc={doc}> | ||
| <DocContext.Provider value={doc}>{props.children}</DocContext.Provider> | ||
| </QuestionCard> | ||
| ); | ||
| }) as React.FC<ChoiceAnswerProps> & ChoiceAnswerSubComponents; |
There was a problem hiding this comment.
After the refactor, this is all that is needed for the Choice Answer :)
| // !! must be a stable reference, otherwise the whole list will re-render on every change | ||
| onChange: (doc: AssessableTypeModelMapping[T], optionIndex: number, checked: boolean) => void; |
There was a problem hiding this comment.
!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; | |||
There was a problem hiding this comment.
prefer using classic, tdev-scoped css variables (--tdev...) instead of scss variables ($tdevAssess...)
| }} | ||
| header={ | ||
| <> | ||
| <h3 className={clsx(styles.questionTitle)}>{doc.displayTitle}</h3> |
There was a problem hiding this comment.
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}> |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
| const useLinkedMetaModel = <Type extends AssessableType>( | ||
| doc: AssessableTypeModelMapping[Type] | undefined | null, | ||
| meta: AssessableMeta<Type> | ||
| ) => { | ||
| React.useEffect(() => { | ||
| (doc as iAssessable<Type> | undefined)?.setLinkedMeta(meta); | ||
| }, [doc, meta]); | ||
| }; |
There was a problem hiding this comment.
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.
| @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; | ||
| } |
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
make the answer components configurable to enable hassle-free extension
| }, | ||
| { | ||
| name: 'Quiz', | ||
| docTypeExtractor: () => 'quiz' | ||
| }, | ||
| { | ||
| name: 'ChoiceAnswer', | ||
| docTypeExtractor: () => 'choice_answer' | ||
| }, | ||
| { | ||
| name: 'TrueFalseAnswer', | ||
| docTypeExtractor: () => 'true_false_answer' |
There was a problem hiding this comment.
needed for the task overview
| if (typesSet.size === 1) { | ||
| return types[0]; | ||
| } | ||
| if (typesSet.has('quiz')) { |
There was a problem hiding this comment.
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
Plan
qidwhen inside Quizqidwhen inside a Quiz (to ensure ordering)