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; | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger