feat(backend): Rendering van meerkeuzevragen en open vragen (essay) toegevoegd + getest
This commit is contained in:
parent
164a547dd1
commit
bc0ac63c92
20 changed files with 126 additions and 16 deletions
|
@ -1,5 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/audio/audio_processor.js
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/audio/audio_processor.js
|
||||||
|
*
|
||||||
|
* WARNING: The support for audio learning objects is currently still experimental.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/extern/extern_processor.js
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/extern/extern_processor.js
|
||||||
|
*
|
||||||
|
* WARNING: The support for external content is currently still experimental.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
|
|
@ -36,18 +36,22 @@ class GiftProcessor extends StringProcessor {
|
||||||
override renderFn(giftString: string) {
|
override renderFn(giftString: string) {
|
||||||
const quizQuestions: GIFTQuestion[] = parse(giftString);
|
const quizQuestions: GIFTQuestion[] = parse(giftString);
|
||||||
|
|
||||||
let html = "<div class='gift'>";
|
let html = "<div class='learning-object-gift'>\n";
|
||||||
|
let i = 1;
|
||||||
for (let question of quizQuestions) {
|
for (let question of quizQuestions) {
|
||||||
html += this.renderQuestion(question);
|
html += ` <div class='gift-question' id='gift-q${i}'>\n`;
|
||||||
|
html += " " + this.renderQuestion(question, i).replaceAll(/\n(.+)/g, "\n $1"); // replace for indentation.
|
||||||
|
html += ` </div>\n`;
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
html += "</div>"
|
html += "</div>\n"
|
||||||
|
|
||||||
return DOMPurify.sanitize(html);
|
return DOMPurify.sanitize(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderQuestion<T extends GIFTQuestion>(question: T): string {
|
private renderQuestion<T extends GIFTQuestion>(question: T, questionNumber: number): string {
|
||||||
const renderer = this.renderers[question.type] as GIFTQuestionRenderer<T>;
|
const renderer = this.renderers[question.type] as GIFTQuestionRenderer<T>;
|
||||||
return renderer.render(question);
|
return renderer.render(question, questionNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {Category} from "gift-pegjs";
|
||||||
import {ProcessingError} from "../../processing-error";
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> {
|
export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> {
|
||||||
render(question: Category): string {
|
render(question: Category, questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'Category' is not supported yet!");
|
throw new ProcessingError("The question type 'Category' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {Description} from "gift-pegjs";
|
||||||
import {ProcessingError} from "../../processing-error";
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> {
|
export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> {
|
||||||
render(question: Description): string {
|
render(question: Description, questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'Description' is not supported yet!");
|
throw new ProcessingError("The question type 'Description' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,15 @@ import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
import {Essay} from "gift-pegjs";
|
import {Essay} from "gift-pegjs";
|
||||||
|
|
||||||
export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> {
|
export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> {
|
||||||
render(question: Essay): string {
|
render(question: Essay, questionNumber: number): string {
|
||||||
return "";
|
let renderedHtml = "";
|
||||||
|
if (question.title) {
|
||||||
|
renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`;
|
||||||
|
}
|
||||||
|
if (question.stem) {
|
||||||
|
renderedHtml += `<p class='gift-stem' id='gift-q${questionNumber}-stem'>${question.stem.text}</p>\n`;
|
||||||
|
}
|
||||||
|
renderedHtml += `<textarea class='gift-essay-answer' id='gift-q${questionNumber}-answer'></textarea>\n`;
|
||||||
|
return renderedHtml;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,8 @@ export abstract class GIFTQuestionRenderer<T extends GIFTQuestion> {
|
||||||
/**
|
/**
|
||||||
* Render the given question to HTML.
|
* Render the given question to HTML.
|
||||||
* @param question The question.
|
* @param question The question.
|
||||||
|
* @param questionNumber The index number of the question.
|
||||||
* @returns The question rendered as HTML.
|
* @returns The question rendered as HTML.
|
||||||
*/
|
*/
|
||||||
abstract render(question: T): string;
|
abstract render(question: T, questionNumber: number): string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {Matching} from "gift-pegjs";
|
||||||
import {ProcessingError} from "../../processing-error";
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
export class MatchingQuestionRenderer extends GIFTQuestionRenderer<Matching> {
|
export class MatchingQuestionRenderer extends GIFTQuestionRenderer<Matching> {
|
||||||
render(question: Matching): string {
|
render(question: Matching, questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'Matching' is not supported yet!");
|
throw new ProcessingError("The question type 'Matching' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,22 @@ import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
import {MultipleChoice} from "gift-pegjs";
|
import {MultipleChoice} from "gift-pegjs";
|
||||||
|
|
||||||
export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<MultipleChoice> {
|
export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<MultipleChoice> {
|
||||||
render(question: MultipleChoice): string {
|
render(question: MultipleChoice, questionNumber: number): string {
|
||||||
return "";
|
let renderedHtml = "";
|
||||||
|
if (question.title) {
|
||||||
|
renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`;
|
||||||
|
}
|
||||||
|
if (question.stem) {
|
||||||
|
renderedHtml += `<p class='gift-stem' id='gift-q${questionNumber}-stem'>${question.stem.text}</p>\n`;
|
||||||
|
}
|
||||||
|
let i = 0;
|
||||||
|
for (let choice of question.choices) {
|
||||||
|
renderedHtml += `<div class="gift-choice-div">\n`;
|
||||||
|
renderedHtml += ` <input type='radio' id='gift-q${questionNumber}-choice-${i}' name='gift-q${questionNumber}-choices' value="${i}"/>\n`;
|
||||||
|
renderedHtml += ` <label for='gift-q${questionNumber}-choice-${i}'>${choice.text}</label>\n`;
|
||||||
|
renderedHtml += `</div>\n`;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return renderedHtml;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {Numerical} from "gift-pegjs";
|
||||||
import {ProcessingError} from "../../processing-error";
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
export class NumericalQuestionRenderer extends GIFTQuestionRenderer<Numerical> {
|
export class NumericalQuestionRenderer extends GIFTQuestionRenderer<Numerical> {
|
||||||
render(question: Numerical): string {
|
render(question: Numerical, questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'Numerical' is not supported yet!");
|
throw new ProcessingError("The question type 'Numerical' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {ShortAnswer} from "gift-pegjs";
|
||||||
import {ProcessingError} from "../../processing-error";
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
export class ShortQuestionRenderer extends GIFTQuestionRenderer<ShortAnswer> {
|
export class ShortQuestionRenderer extends GIFTQuestionRenderer<ShortAnswer> {
|
||||||
render(question: ShortAnswer): string {
|
render(question: ShortAnswer, questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'ShortAnswer' is not supported yet!");
|
throw new ProcessingError("The question type 'ShortAnswer' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {TrueFalse} from "gift-pegjs";
|
||||||
import {ProcessingError} from "../../processing-error";
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
export class TrueFalseQuestionRenderer extends GIFTQuestionRenderer<TrueFalse> {
|
export class TrueFalseQuestionRenderer extends GIFTQuestionRenderer<TrueFalse> {
|
||||||
render(question: TrueFalse): string {
|
render(question: TrueFalse, questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'TrueFalse' is not supported yet!");
|
throw new ProcessingError("The question type 'TrueFalse' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/pdf/pdf_processor.js
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/pdf/pdf_processor.js
|
||||||
|
*
|
||||||
|
* WARNING: The support for PDF learning objects is currently still experimental.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import {describe, expect, it} from "vitest";
|
||||||
|
import mdExample from "../../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example";
|
||||||
|
import multipleChoiceExample from "../../../test-assets/learning-objects/test-multiple-choice/test-multiple-choice-example";
|
||||||
|
import essayExample from "../../../test-assets/learning-objects/test-essay/test-essay-example";
|
||||||
|
import processingService from "../../../../src/services/learning-objects/processing/processing-service";
|
||||||
|
|
||||||
|
describe("ProcessingService", () => {
|
||||||
|
it("renders a markdown learning object correctly", async () => {
|
||||||
|
const markdownLearningObject = mdExample.createLearningObject();
|
||||||
|
const result = await processingService.render(markdownLearningObject);
|
||||||
|
expect(result).toEqual(mdExample.getHTMLRendering());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a multiple choice question correctly", async () => {
|
||||||
|
const multipleChoiceLearningObject = multipleChoiceExample.createLearningObject();
|
||||||
|
const result = await processingService.render(multipleChoiceLearningObject);
|
||||||
|
expect(result).toEqual(multipleChoiceExample.getHTMLRendering());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders an essay question correctly", async () => {
|
||||||
|
const essayLearningObject = essayExample.createLearningObject();
|
||||||
|
const result = await processingService.render(essayLearningObject);
|
||||||
|
expect(result).toEqual(essayExample.getHTMLRendering());
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,6 +2,7 @@ import {LearningObjectExample} from "../learning-object-example";
|
||||||
import {LearningObject} from "../../../../src/entities/content/learning-object.entity";
|
import {LearningObject} from "../../../../src/entities/content/learning-object.entity";
|
||||||
import {Language} from "../../../../src/entities/content/language";
|
import {Language} from "../../../../src/entities/content/language";
|
||||||
import {loadTestAsset} from "../../../test-utils/load-test-asset";
|
import {loadTestAsset} from "../../../test-utils/load-test-asset";
|
||||||
|
import {DwengoContentType} from "../../../../src/services/learning-objects/processing/content-type";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a dummy learning object to be used in tests where multiple learning objects are needed (for example for use
|
* Create a dummy learning object to be used in tests where multiple learning objects are needed (for example for use
|
||||||
|
@ -16,6 +17,7 @@ export function dummyLearningObject(hruid: string, language: Language, title: st
|
||||||
learningObject.version = 1;
|
learningObject.version = 1;
|
||||||
learningObject.title = title;
|
learningObject.title = title;
|
||||||
learningObject.description = "Just a dummy learning object for testing purposes";
|
learningObject.description = "Just a dummy learning object for testing purposes";
|
||||||
|
learningObject.contentType = DwengoContentType.TEXT_PLAIN;
|
||||||
learningObject.content = Buffer.from("Dummy content");
|
learningObject.content = Buffer.from("Dummy content");
|
||||||
return learningObject;
|
return learningObject;
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
::MC basic::
|
||||||
|
How are you? {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div class="learning-object-gift">
|
||||||
|
<div id="gift-q1" class="gift-question">
|
||||||
|
<h2 id="gift-q1-title" class="gift-title">MC basic</h2>
|
||||||
|
<p id="gift-q1-stem" class="gift-stem">How are you?</p>
|
||||||
|
<textarea id="gift-q1-answer" class="gift-essay-answer"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,24 @@
|
||||||
|
import {LearningObjectExample} from "../learning-object-example";
|
||||||
|
import {LearningObject} from "../../../../src/entities/content/learning-object.entity";
|
||||||
|
import {loadTestAsset} from "../../../test-utils/load-test-asset";
|
||||||
|
import {EnvVars, getEnvVar} from "../../../../src/util/envvars";
|
||||||
|
import {Language} from "../../../../src/entities/content/language";
|
||||||
|
import {DwengoContentType} from "../../../../src/services/learning-objects/processing/content-type";
|
||||||
|
|
||||||
|
const example: LearningObjectExample = {
|
||||||
|
createLearningObject: () => {
|
||||||
|
const learningObject = new LearningObject();
|
||||||
|
learningObject.hruid = `${getEnvVar(EnvVars.UserContentPrefix)}test_essay`;
|
||||||
|
learningObject.language = Language.English;
|
||||||
|
learningObject.version = 1;
|
||||||
|
learningObject.title = "Essay question for testing";
|
||||||
|
learningObject.description = "This essay question was only created for testing purposes.";
|
||||||
|
learningObject.contentType = DwengoContentType.GIFT;
|
||||||
|
learningObject.content = loadTestAsset("learning-objects/test-essay/content.txt");
|
||||||
|
return learningObject;
|
||||||
|
},
|
||||||
|
createAttachment: {},
|
||||||
|
getHTMLRendering: () => loadTestAsset("learning-objects/test-essay/rendering.html").toString()
|
||||||
|
};
|
||||||
|
|
||||||
|
export default example;
|
|
@ -0,0 +1,14 @@
|
||||||
|
<div class="learning-object-gift">
|
||||||
|
<div id="gift-q1" class="gift-question">
|
||||||
|
<h2 id="gift-q1-title" class="gift-title">MC basic</h2>
|
||||||
|
<p id="gift-q1-stem" class="gift-stem">Are you following along well with the class?</p>
|
||||||
|
<div class="gift-choice-div">
|
||||||
|
<input value="0" name="gift-q1-choices" id="gift-q1-choice-0" type="radio">
|
||||||
|
<label for="gift-q1-choice-0">[object Object]</label>
|
||||||
|
</div>
|
||||||
|
<div class="gift-choice-div">
|
||||||
|
<input value="1" name="gift-q1-choices" id="gift-q1-choice-1" type="radio">
|
||||||
|
<label for="gift-q1-choice-1">[object Object]</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -3,6 +3,7 @@ import {LearningObject} from "../../../../src/entities/content/learning-object.e
|
||||||
import {loadTestAsset} from "../../../test-utils/load-test-asset";
|
import {loadTestAsset} from "../../../test-utils/load-test-asset";
|
||||||
import {EnvVars, getEnvVar} from "../../../../src/util/envvars";
|
import {EnvVars, getEnvVar} from "../../../../src/util/envvars";
|
||||||
import {Language} from "../../../../src/entities/content/language";
|
import {Language} from "../../../../src/entities/content/language";
|
||||||
|
import {DwengoContentType} from "../../../../src/services/learning-objects/processing/content-type";
|
||||||
|
|
||||||
const example: LearningObjectExample = {
|
const example: LearningObjectExample = {
|
||||||
createLearningObject: () => {
|
createLearningObject: () => {
|
||||||
|
@ -12,6 +13,7 @@ const example: LearningObjectExample = {
|
||||||
learningObject.version = 1;
|
learningObject.version = 1;
|
||||||
learningObject.title = "Multiple choice question for testing";
|
learningObject.title = "Multiple choice question for testing";
|
||||||
learningObject.description = "This multiple choice question was only created for testing purposes.";
|
learningObject.description = "This multiple choice question was only created for testing purposes.";
|
||||||
|
learningObject.contentType = DwengoContentType.GIFT;
|
||||||
learningObject.content = loadTestAsset("learning-objects/test-multiple-choice/content.txt");
|
learningObject.content = loadTestAsset("learning-objects/test-multiple-choice/content.txt");
|
||||||
return learningObject;
|
return learningObject;
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue