merge: merged into feat/error-flow-backend

This commit is contained in:
Adriaan Jacquet 2025-04-06 17:49:55 +02:00
commit effaeb0277
249 changed files with 6832 additions and 3679 deletions

View file

@ -1,10 +1,13 @@
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import { getAssignmentRepository, getClassRepository, getGroupRepository, getQuestionRepository, getSubmissionRepository } from '../data/repositories.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
import { mapToQuestionDTO } from '../interfaces/question.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { fetchClass } from './classes.js';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
export async function fetchAssignment(classid: string, assignmentNumber: number): Promise<Assignment> {
const classRepository = getClassRepository();
@ -37,35 +40,21 @@ export async function getAllAssignments(classid: string, full: boolean): Promise
return assignments.map(mapToAssignmentDTOId);
}
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO | null> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
if (!cls) {
return null;
}
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO> {
const cls = await fetchClass(classid);
const assignment = mapToAssignment(assignmentData, cls);
const assignmentRepository = getAssignmentRepository();
try {
const newAssignment = assignmentRepository.create(assignment);
await assignmentRepository.save(newAssignment);
const newAssignment = assignmentRepository.create(assignment);
await assignmentRepository.save(newAssignment, {preventOverwrite: true});
return mapToAssignmentDTO(newAssignment);
return mapToAssignmentDTO(newAssignment);
} catch (e) {
console.error(e);
return null;
}
}
export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO | null> {
export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO> {
const assignment = await fetchAssignment(classid, id);
if (!assignment) {
return null;
}
return mapToAssignmentDTO(assignment);
}
@ -76,15 +65,15 @@ export async function getAssignmentsSubmissions(
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const assignment = await fetchAssignment(classid, assignmentNumber);
if (!assignment) {
return [];
}
const groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
if (groups.length === 0){
throw new NotFoundException('No groups for assignment found');
}
const submissionRepository = getSubmissionRepository();
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
const submissions = (await Promise.all(groups.map(async (group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
if (full) {
return submissions.map(mapToSubmissionDTO);
@ -100,10 +89,6 @@ export async function getAssignmentsQuestions(
): Promise<QuestionDTO[] | QuestionId[]> {
const assignment = await fetchAssignment(classid, assignmentNumber);
if (!assignment) {
return [];
}
const questionRepository = getQuestionRepository();
const questions = await questionRepository.findAllByAssignment(assignment);
@ -111,5 +96,5 @@ export async function getAssignmentsQuestions(
return questions.map(mapToQuestionDTO);
}
return questions.map(mapToQuestionDTO).map(mapToQuestionId); // mapToQuestionId should be updated
return questions.map(mapToQuestionDTO); // mapToQuestionId should be updated
}

View file

@ -1,12 +1,17 @@
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
import { Class } from '../entities/classes/class.entity.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js';
import { mapToClassDTO } from '../interfaces/class.js';
import { mapToStudentDTO } from '../interfaces/student.js';
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js';
import { getLogger } from '../logging/initalize.js';
const logger = getLogger();
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { Class } from '../entities/classes/class.entity.js';
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation';
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import {fetchTeacher} from "./teachers";
import {fetchStudent} from "./students";
import {TeacherDTO} from "@dwengo-1/common/interfaces/teacher";
import {mapToTeacherDTO} from "../interfaces/teacher";
export async function fetchClass(classid: string): Promise<Class> {
const classRepository = getClassRepository();
@ -36,38 +41,27 @@ export async function getClass(classId: string): Promise<ClassDTO | null> {
}
export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> {
const teacherRepository = getTeacherRepository();
const teacherUsernames = classData.teachers || [];
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null);
const teachers = (await Promise.all(teacherUsernames.map(async (id) => fetchTeacher(id) )));
const studentRepository = getStudentRepository();
const studentUsernames = classData.students || [];
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null);
const students = (await Promise.all(studentUsernames.map(async (id) => fetchStudent(id) )));
const classRepository = getClassRepository();
try {
const newClass = classRepository.create({
displayName: classData.displayName,
teachers: teachers,
students: students,
});
await classRepository.save(newClass);
const newClass = classRepository.create({
displayName: classData.displayName,
teachers: teachers,
students: students,
});
await classRepository.save(newClass, {preventOverwrite: true});
return mapToClassDTO(newClass);
} catch (e) {
logger.error(e);
return null;
}
return mapToClassDTO(newClass);
}
export async function deleteClass(classId: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
if (!cls) {
throw new NotFoundException('Could not delete class because it does not exist');
}
const classRepository = getClassRepository();
await classRepository.deleteById(classId);
@ -80,10 +74,23 @@ export async function getClassStudents(classId: string, full: boolean): Promise<
if (full) {
return cls.students.map(mapToStudentDTO);
}
return cls.students.map((student) => student.username);
}
export async function getClassStudentsDTO(classId: string): Promise<StudentDTO[]> {
const cls = await fetchClass(classId);
return cls.students.map(mapToStudentDTO);
}
export async function getClassTeachers(classId: string, full: boolean): Promise<TeacherDTO[] | string[]> {
const cls = await fetchClass(classId);
if (full){
return cls.teachers.map(mapToTeacherDTO);
}
return cls.teachers.map((student) => student.username);
}
export async function getClassTeacherInvitations(classId: string, full: boolean): Promise<TeacherInvitationDTO[]> {
const cls = await fetchClass(classId);

View file

@ -8,10 +8,12 @@ import {
getSubmissionRepository,
} from '../data/repositories.js';
import { Group } from '../entities/assignments/group.entity.js';
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { getLogger } from '../logging/initalize.js';
import { fetchAssignment } from './assignments.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<Group | null> {
const assignment = await fetchAssignment(classId, assignmentNumber);
@ -44,9 +46,11 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
const studentRepository = getStudentRepository();
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null);
const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student !== null
);
console.log(members);
getLogger().debug(members);
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
@ -72,7 +76,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
return newGroup;
} catch (e) {
console.log(e);
getLogger().error(e);
return null;
}
}

View file

@ -1,6 +1,13 @@
import { DWENGO_API_BASE } from '../config.js';
import { fetchWithLogging } from '../util/api-helper.js';
import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js';
import {
FilteredLearningObject,
LearningObjectMetadata,
LearningObjectNode,
LearningPathResponse,
} from '@dwengo-1/common/interfaces/learning-content';
import { getLogger } from '../logging/initalize.js';
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
return {
@ -37,7 +44,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr
);
if (!metadata) {
console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`);
getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`);
return null;
}
@ -48,7 +55,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr
/**
* Generic function to fetch learning paths
*/
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
function fetchLearningPaths(_arg0: string[], _language: string, _arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
throw new Error('Function not implemented.');
}
@ -60,7 +67,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri
const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`);
if (!learningPathResponse.success || !learningPathResponse.data?.length) {
console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`);
getLogger().error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`);
return [];
}
@ -74,7 +81,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri
objects.filter((obj): obj is FilteredLearningObject => obj !== null)
);
} catch (error) {
console.error('❌ Error fetching learning objects:', error);
getLogger().error('❌ Error fetching learning objects:', error);
return [];
}
}

View file

@ -1,9 +1,10 @@
import { getAttachmentRepository } from '../../data/repositories.js';
import { Attachment } from '../../entities/content/attachment.entity.js';
import { LearningObjectIdentifier } from '../../interfaces/learning-content.js';
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content';
const attachmentService = {
getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
const attachmentRepo = getAttachmentRepository();
if (learningObjectId.version) {

View file

@ -1,13 +1,12 @@
import { LearningObjectProvider } from './learning-object-provider.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js';
import { Language } from '../../entities/content/language.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { getUrlStringForLearningObject } from '../../util/links.js';
import processingService from './processing/processing-service.js';
import { NotFoundError } from '@mikro-orm/core';
import learningObjectService from './learning-object-service.js';
import { getLogger, Logger } from '../../logging/initalize.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
const logger: Logger = getLogger();
@ -41,10 +40,10 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
};
}
function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> {
async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> {
const learningObjectRepo = getLearningObjectRepository();
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language);
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
}
/**
@ -65,11 +64,11 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
const learningObjectRepo = getLearningObjectRepository();
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language);
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
if (!learningObject) {
return null;
}
return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id));
return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id));
},
/**
@ -96,7 +95,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
throw new NotFoundError('The learning path with the given ID could not be found.');
}
const learningObjects = await Promise.all(
learningPath.nodes.map((it) => {
learningPath.nodes.map(async (it) => {
const learningObject = learningObjectService.getLearningObjectById({
hruid: it.learningObjectHruid,
language: it.language,

View file

@ -1,5 +1,8 @@
import { DWENGO_API_BASE } from '../../config.js';
import { fetchWithLogging } from '../../util/api-helper.js';
import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js';
import { LearningObjectProvider } from './learning-object-provider.js';
import { getLogger, Logger } from '../../logging/initalize.js';
import {
FilteredLearningObject,
LearningObjectIdentifier,
@ -7,10 +10,7 @@ import {
LearningObjectNode,
LearningPathIdentifier,
LearningPathResponse,
} from '../../interfaces/learning-content.js';
import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js';
import { LearningObjectProvider } from './learning-object-provider.js';
import { getLogger, Logger } from '../../logging/initalize.js';
} from '@dwengo-1/common/interfaces/learning-content';
const logger: Logger = getLogger();
@ -66,12 +66,13 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full
}
const objects = await Promise.all(
nodes.map(async (node) =>
dwengoApiLearningObjectProvider.getLearningObjectById({
nodes.map(async (node) => {
const learningObjectId: LearningObjectIdentifier = {
hruid: node.learningobject_hruid,
language: learningPathId.language,
})
)
};
return dwengoApiLearningObjectProvider.getLearningObjectById(learningObjectId);
})
);
return objects.filter((obj): obj is FilteredLearningObject => obj !== null);
} catch (error) {
@ -90,7 +91,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
metadataUrl,
`Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`,
{
params: id,
params: { ...id },
}
);
@ -123,7 +124,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`;
const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, {
params: id,
params: { ...id },
});
if (!html) {

View file

@ -1,4 +1,4 @@
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
export interface LearningObjectProvider {
/**

View file

@ -1,11 +1,11 @@
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js';
import { LearningObjectProvider } from './learning-object-provider.js';
import { EnvVars, getEnvVar } from '../../util/envvars.js';
import { envVars, getEnvVar } from '../../util/envVars.js';
import databaseLearningObjectProvider from './database-learning-object-provider.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
function getProvider(id: LearningObjectIdentifier): LearningObjectProvider {
if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) {
if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
return databaseLearningObjectProvider;
}
return dwengoApiLearningObjectProvider;
@ -18,28 +18,28 @@ const learningObjectService = {
/**
* Fetches a single learning object by its HRUID
*/
getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> {
async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> {
return getProvider(id).getLearningObjectById(id);
},
/**
* Fetch full learning object data (metadata)
*/
getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> {
async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> {
return getProvider(id).getLearningObjectsFromPath(id);
},
/**
* Fetch only learning object HRUIDs
*/
getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> {
async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> {
return getProvider(id).getLearningObjectIdsFromPath(id);
},
/**
* Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/
getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
return getProvider(id).getLearningObjectHTML(id);
},
};

View file

@ -14,7 +14,7 @@ class AudioProcessor extends StringProcessor {
super(DwengoContentType.AUDIO_MPEG);
}
protected renderFn(audioUrl: string): string {
override renderFn(audioUrl: string): string {
return DOMPurify.sanitize(`<audio controls>
<source src="${audioUrl}" type=${type}>
Your browser does not support the audio element.

View file

@ -15,7 +15,7 @@ class ExternProcessor extends StringProcessor {
super(DwengoContentType.EXTERN);
}
override renderFn(externURL: string) {
override renderFn(externURL: string): string {
if (!isValidHttpUrl(externURL)) {
throw new ProcessingError('The url is not valid: ' + externURL);
}

View file

@ -32,7 +32,7 @@ class GiftProcessor extends StringProcessor {
super(DwengoContentType.GIFT);
}
override renderFn(giftString: string) {
override renderFn(giftString: string): string {
const quizQuestions: GIFTQuestion[] = parse(giftString);
let html = "<div class='learning-object-gift'>\n";

View file

@ -3,7 +3,7 @@ import { Category } from 'gift-pegjs';
import { ProcessingError } from '../../processing-error.js';
export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> {
render(question: Category, questionNumber: number): string {
override render(_question: Category, _questionNumber: number): string {
throw new ProcessingError("The question type 'Category' is not supported yet!");
}
}

View file

@ -3,7 +3,7 @@ import { Description } from 'gift-pegjs';
import { ProcessingError } from '../../processing-error.js';
export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> {
render(question: Description, questionNumber: number): string {
override render(_question: Description, _questionNumber: number): string {
throw new ProcessingError("The question type 'Description' is not supported yet!");
}
}

View file

@ -2,7 +2,7 @@ import { GIFTQuestionRenderer } from './gift-question-renderer.js';
import { Essay } from 'gift-pegjs';
export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> {
render(question: Essay, questionNumber: number): string {
override render(question: Essay, questionNumber: number): string {
let renderedHtml = '';
if (question.title) {
renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`;

View file

@ -3,7 +3,7 @@ import { Matching } from 'gift-pegjs';
import { ProcessingError } from '../../processing-error.js';
export class MatchingQuestionRenderer extends GIFTQuestionRenderer<Matching> {
render(question: Matching, questionNumber: number): string {
override render(_question: Matching, _questionNumber: number): string {
throw new ProcessingError("The question type 'Matching' is not supported yet!");
}
}

View file

@ -2,7 +2,7 @@ import { GIFTQuestionRenderer } from './gift-question-renderer.js';
import { MultipleChoice } from 'gift-pegjs';
export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<MultipleChoice> {
render(question: MultipleChoice, questionNumber: number): string {
override render(question: MultipleChoice, questionNumber: number): string {
let renderedHtml = '';
if (question.title) {
renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`;

View file

@ -3,7 +3,7 @@ import { Numerical } from 'gift-pegjs';
import { ProcessingError } from '../../processing-error.js';
export class NumericalQuestionRenderer extends GIFTQuestionRenderer<Numerical> {
render(question: Numerical, questionNumber: number): string {
override render(_question: Numerical, _questionNumber: number): string {
throw new ProcessingError("The question type 'Numerical' is not supported yet!");
}
}

View file

@ -3,7 +3,7 @@ import { ShortAnswer } from 'gift-pegjs';
import { ProcessingError } from '../../processing-error.js';
export class ShortQuestionRenderer extends GIFTQuestionRenderer<ShortAnswer> {
render(question: ShortAnswer, questionNumber: number): string {
override render(_question: ShortAnswer, _questionNumber: number): string {
throw new ProcessingError("The question type 'ShortAnswer' is not supported yet!");
}
}

View file

@ -3,7 +3,7 @@ import { TrueFalse } from 'gift-pegjs';
import { ProcessingError } from '../../processing-error.js';
export class TrueFalseQuestionRenderer extends GIFTQuestionRenderer<TrueFalse> {
render(question: TrueFalse, questionNumber: number): string {
override render(_question: TrueFalse, _questionNumber: number): string {
throw new ProcessingError("The question type 'TrueFalse' is not supported yet!");
}
}

View file

@ -10,7 +10,7 @@ class BlockImageProcessor extends InlineImageProcessor {
super();
}
override renderFn(imageUrl: string) {
override renderFn(imageUrl: string): string {
const inlineHtml = super.render(imageUrl);
return DOMPurify.sanitize(`<div>${inlineHtml}</div>`);
}

View file

@ -13,7 +13,7 @@ class InlineImageProcessor extends StringProcessor {
super(contentType);
}
override renderFn(imageUrl: string) {
override renderFn(imageUrl: string): string {
if (!isValidHttpUrl(imageUrl)) {
throw new ProcessingError(`Image URL is invalid: ${imageUrl}`);
}

View file

@ -8,13 +8,12 @@ import InlineImageProcessor from '../image/inline-image-processor.js';
import * as marked from 'marked';
import { getUrlStringForLearningObjectHTML, isValidHttpUrl } from '../../../../util/links.js';
import { ProcessingError } from '../processing-error.js';
import { LearningObjectIdentifier } from '../../../../interfaces/learning-content.js';
import { Language } from '../../../../entities/content/language.js';
import Image = marked.Tokens.Image;
import Heading = marked.Tokens.Heading;
import Link = marked.Tokens.Link;
import RendererObject = marked.RendererObject;
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language';
const prefixes = {
learningObject: '@learning-object',

View file

@ -14,26 +14,24 @@ class MarkdownProcessor extends StringProcessor {
super(DwengoContentType.TEXT_MARKDOWN);
}
override renderFn(mdText: string) {
let html = '';
try {
marked.use({ renderer: dwengoMarkedRenderer });
html = marked(mdText, { async: false });
html = this.replaceLinks(html); // Replace html image links path
} catch (e: any) {
throw new ProcessingError(e.message);
}
return html;
}
replaceLinks(html: string) {
static replaceLinks(html: string): string {
const proc = new InlineImageProcessor();
html = html.replace(
/<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g,
(match: string, src: string, alt: string, altText: string, title: string, titleText: string) => proc.render(src)
(_match: string, src: string, _alt: string, _altText: string, _title: string, _titleText: string) => proc.render(src)
);
return html;
}
override renderFn(mdText: string): string {
try {
marked.use({ renderer: dwengoMarkedRenderer });
const html = marked(mdText, { async: false });
return MarkdownProcessor.replaceLinks(html); // Replace html image links path
} catch (e: unknown) {
throw new ProcessingError('Unknown error while processing markdown: ' + e);
}
}
}
export { MarkdownProcessor };

View file

@ -15,7 +15,7 @@ class PdfProcessor extends StringProcessor {
super(DwengoContentType.APPLICATION_PDF);
}
override renderFn(pdfUrl: string) {
override renderFn(pdfUrl: string): string {
if (!isValidHttpUrl(pdfUrl)) {
throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`);
}

View file

@ -13,15 +13,15 @@ import GiftProcessor from './gift/gift-processor.js';
import { LearningObject } from '../../../entities/content/learning-object.entity.js';
import Processor from './processor.js';
import { DwengoContentType } from './content-type.js';
import { LearningObjectIdentifier } from '../../../interfaces/learning-content.js';
import { Language } from '../../../entities/content/language.js';
import { replaceAsync } from '../../../util/async.js';
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language';
const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" language="([^"]+)" version="([^"]+)"\/>/g;
const LEARNING_OBJECT_DOES_NOT_EXIST = "<div class='non-existing-learning-object' />";
class ProcessingService {
private processors!: Map<DwengoContentType, Processor<any>>;
private processors!: Map<DwengoContentType, Processor<DwengoContentType>>;
constructor() {
const processors = [

View file

@ -9,7 +9,9 @@ import { DwengoContentType } from './content-type.js';
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processor.js
*/
abstract class Processor<T> {
protected constructor(public contentType: DwengoContentType) {}
protected constructor(public contentType: DwengoContentType) {
// Do nothing
}
/**
* Render the given object.

View file

@ -11,7 +11,7 @@ class TextProcessor extends StringProcessor {
super(DwengoContentType.TEXT_PLAIN);
}
override renderFn(text: string) {
override renderFn(text: string): string {
// Sanitize plain text to prevent xss.
return DOMPurify.sanitize(text);
}

View file

@ -1,12 +1,18 @@
import { LearningPathProvider } from './learning-path-provider.js';
import { FilteredLearningObject, LearningObjectNode, LearningPath, LearningPathResponse, Transition } from '../../interfaces/learning-content.js';
import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity.js';
import { getLearningPathRepository } from '../../data/repositories.js';
import { Language } from '../../entities/content/language.js';
import learningObjectService from '../learning-objects/learning-object-service.js';
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
import { getLastSubmissionForCustomizationTarget, isTransitionPossible, PersonalizationTarget } from './learning-path-personalization-util.js';
import {
FilteredLearningObject,
LearningObjectNode,
LearningPath,
LearningPathResponse,
Transition,
} from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language';
/**
* Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its
@ -18,14 +24,14 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
// Its corresponding learning object.
const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>(
await Promise.all(
nodes.map((node) =>
nodes.map(async (node) =>
learningObjectService
.getLearningObjectById({
hruid: node.learningObjectHruid,
version: node.version,
language: node.language,
})
.then((learningObject) => <[LearningPathNode, FilteredLearningObject | null]>[node, learningObject])
.then((learningObject) => [node, learningObject] as [LearningPathNode, FilteredLearningObject | null])
)
)
);
@ -117,7 +123,7 @@ async function convertNodes(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget
): Promise<LearningObjectNode[]> {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map((entry) =>
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) =>
convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects)
);
return await Promise.all(nodesPromise);
@ -152,7 +158,7 @@ function convertTransition(
throw new Error(`Learning object ${transition.next.learningObjectHruid}/${transition.next.language}/${transition.next.version} not found!`);
} else {
return {
_id: '' + index, // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
_id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
default: false, // We don't work with default transitions but retain this for backwards compatibility.
next: {
_id: nextNode._id + index, // Construct a unique ID for the transition for backwards compatibility.
@ -179,11 +185,11 @@ const databaseLearningPathProvider: LearningPathProvider = {
): Promise<LearningPathResponse> {
const learningPathRepo = getLearningPathRepository();
const learningPaths = (await Promise.all(hruids.map((hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter(
const learningPaths = (await Promise.all(hruids.map(async (hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter(
(learningPath) => learningPath !== null
);
const filteredLearningPaths = await Promise.all(
learningPaths.map((learningPath, index) => convertLearningPath(learningPath, index, personalizedFor))
learningPaths.map(async (learningPath, index) => convertLearningPath(learningPath, index, personalizedFor))
);
return {
@ -200,7 +206,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
const learningPathRepo = getLearningPathRepository();
const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language);
return await Promise.all(searchResults.map((result, index) => convertLearningPath(result, index, personalizedFor)));
return await Promise.all(searchResults.map(async (result, index) => convertLearningPath(result, index, personalizedFor)));
},
};

View file

@ -1,8 +1,8 @@
import { fetchWithLogging } from '../../util/api-helper.js';
import { DWENGO_API_BASE } from '../../config.js';
import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js';
import { LearningPathProvider } from './learning-path-provider.js';
import { getLogger, Logger } from '../../logging/initalize.js';
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
const logger: Logger = getLogger();

View file

@ -1,6 +1,6 @@
import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js';
import { Language } from '../../entities/content/language.js';
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { PersonalizationTarget } from './learning-path-personalization-util.js';
import { Language } from '@dwengo-1/common/util/language';
/**
* Generic interface for a service which provides access to learning paths from a data source.

View file

@ -1,11 +1,11 @@
import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js';
import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js';
import databaseLearningPathProvider from './database-learning-path-provider.js';
import { EnvVars, getEnvVar } from '../../util/envvars.js';
import { Language } from '../../entities/content/language.js';
import { envVars, getEnvVar } from '../../util/envVars.js';
import { PersonalizationTarget } from './learning-path-personalization-util.js';
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language';
const userContentPrefix = getEnvVar(EnvVars.UserContentPrefix);
const userContentPrefix = getEnvVar(envVars.UserContentPrefix);
const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider];
/**
@ -49,7 +49,9 @@ const learningPathService = {
* Search learning paths in the data source using the given search string.
*/
async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> {
const providerResponses = await Promise.all(allProviders.map((provider) => provider.searchLearningPaths(query, language, personalizedFor)));
const providerResponses = await Promise.all(
allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor))
);
return providerResponses.flat();
},
};

View file

@ -1,13 +1,13 @@
import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
import { Question } from '../entities/questions/question.entity.js';
import { Answer } from '../entities/questions/answer.entity.js';
import { mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js';
import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js';
import { QuestionRepository } from '../data/questions/question-repository.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToUser } from '../interfaces/user.js';
import { Student } from '../entities/users/student.entity.js';
import { mapToStudent } from '../interfaces/student.js';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const questionRepository: QuestionRepository = getQuestionRepository();
@ -17,13 +17,11 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea
return [];
}
const questionsDTO: QuestionDTO[] = questions.map(mapToQuestionDTO);
if (full) {
return questionsDTO;
return questions.map(mapToQuestionDTO);
}
return questionsDTO.map(mapToQuestionId);
return questions.map(mapToQuestionDTOId);
}
async function fetchQuestion(questionId: QuestionId): Promise<Question | null> {
@ -47,7 +45,7 @@ export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO |
return mapToQuestionDTO(question);
}
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean) {
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise<AnswerDTO[] | AnswerId[]> {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
@ -61,34 +59,37 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean
return [];
}
const answersDTO = answers.map(mapToAnswerDTO);
if (full) {
return answersDTO;
return answers.map(mapToAnswerDTO);
}
return answersDTO.map(mapToAnswerId);
return answers.map(mapToAnswerDTOId);
}
export async function createQuestion(questionDTO: QuestionDTO) {
export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> {
const questionRepository = getQuestionRepository();
const author = mapToStudent(questionDTO.author);
const loId: LearningObjectIdentifier = {
...questionDTO.learningObjectIdentifier,
version: questionDTO.learningObjectIdentifier.version ?? 1,
};
try {
await questionRepository.createQuestion({
loId: questionDTO.learningObjectIdentifier,
loId,
author,
content: questionDTO.content,
});
} catch (e) {
} catch (_) {
return null;
}
return questionDTO;
}
export async function deleteQuestion(questionId: QuestionId) {
export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO | null> {
const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId);
@ -97,9 +98,14 @@ export async function deleteQuestion(questionId: QuestionId) {
return null;
}
const loId: LearningObjectIdentifier = {
...questionId.learningObjectIdentifier,
version: questionId.learningObjectIdentifier.version ?? 1,
};
try {
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(questionId.learningObjectIdentifier, questionId.sequenceNumber);
} catch (e) {
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(loId, questionId.sequenceNumber);
} catch (_) {
return null;
}

View file

@ -1,62 +1,75 @@
import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js';
import { AssignmentDTO } from '../interfaces/assignment.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
import {
getClassJoinRequestRepository,
getClassRepository,
getGroupRepository,
getQuestionRepository,
getStudentRepository,
getSubmissionRepository,
} from '../data/repositories.js';
import { mapToClassDTO } from '../interfaces/class.js';
import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { getAllAssignments } from './assignments.js';
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
import { mapToStudentRequest, mapToStudentRequestDTO } from '../interfaces/student-request.js';
import { Student } from '../entities/users/student.entity.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { fetchClass } from './classes.js';
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
const studentRepository = getStudentRepository();
const students = await studentRepository.findAll();
const users = await studentRepository.findAll();
if (full) {
return students.map(mapToStudentDTO);
return users.map(mapToStudentDTO);
}
return students.map((student) => student.username);
return users.map((user) => user.username);
}
export async function getStudent(username: string): Promise<StudentDTO | null> {
export async function fetchStudent(username: string): Promise<Student> {
const studentRepository = getStudentRepository();
const user = await studentRepository.findByUsername(username);
return user ? mapToStudentDTO(user) : null;
if (!user) {
throw new NotFoundException('Student with username not found');
}
return user;
}
export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> {
export async function getStudent(username: string): Promise<StudentDTO> {
const user = await fetchStudent(username);
return mapToStudentDTO(user);
}
export async function createStudent(userData: StudentDTO): Promise<StudentDTO> {
const studentRepository = getStudentRepository();
const newStudent = mapToStudent(userData);
await studentRepository.save(newStudent, { preventOverwrite: true });
return mapToStudentDTO(newStudent);
return userData;
}
export async function deleteStudent(username: string): Promise<StudentDTO | null> {
export async function deleteStudent(username: string): Promise<StudentDTO> {
const studentRepository = getStudentRepository();
const user = await studentRepository.findByUsername(username);
const student = await fetchStudent(username); // Throws error if it does not exist
if (!user) {
return null;
}
try {
await studentRepository.deleteByUsername(username);
return mapToStudentDTO(user);
} catch (e) {
console.log(e);
return null;
}
await studentRepository.deleteByUsername(username);
return mapToStudentDTO(student);
}
export async function getStudentClasses(username: string, full: boolean): Promise<ClassDTO[] | string[]> {
const studentRepository = getStudentRepository();
const student = await studentRepository.findByUsername(username);
if (!student) {
return [];
}
const student = await fetchStudent(username);
const classRepository = getClassRepository();
const classes = await classRepository.findByStudent(student);
@ -69,12 +82,7 @@ export async function getStudentClasses(username: string, full: boolean): Promis
}
export async function getStudentAssignments(username: string, full: boolean): Promise<AssignmentDTO[]> {
const studentRepository = getStudentRepository();
const student = await studentRepository.findByUsername(username);
if (!student) {
return [];
}
const student = await fetchStudent(username);
const classRepository = getClassRepository();
const classes = await classRepository.findByStudent(student);
@ -83,12 +91,7 @@ export async function getStudentAssignments(username: string, full: boolean): Pr
}
export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> {
const studentRepository = getStudentRepository();
const student = await studentRepository.findByUsername(username);
if (!student) {
return [];
}
const student = await fetchStudent(username);
const groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsWithStudent(student);
@ -101,12 +104,7 @@ export async function getStudentGroups(username: string, full: boolean): Promise
}
export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const studentRepository = getStudentRepository();
const student = await studentRepository.findByUsername(username);
if (!student) {
return [];
}
const student = await fetchStudent(username);
const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForStudent(student);
@ -117,3 +115,66 @@ export async function getStudentSubmissions(username: string, full: boolean): Pr
return submissions.map(mapToSubmissionDTOId);
}
export async function getStudentQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const student = await fetchStudent(username);
const questionRepository = getQuestionRepository();
const questions = await questionRepository.findAllByAuthor(student);
if (full) {
return questions.map(mapToQuestionDTO);
}
return questions.map(mapToQuestionDTOId);
}
export async function createClassJoinRequest(username: string, classId: string): Promise<ClassJoinRequestDTO> {
const requestRepo = getClassJoinRequestRepository();
const student = await fetchStudent(username); // Throws error if student not found
const cls = await fetchClass(classId);
const request = mapToStudentRequest(student, cls);
await requestRepo.save(request, { preventOverwrite: true });
return mapToStudentRequestDTO(request);
}
export async function getJoinRequestsByStudent(username: string): Promise<ClassJoinRequestDTO[]> {
const requestRepo = getClassJoinRequestRepository();
const student = await fetchStudent(username);
const requests = await requestRepo.findAllRequestsBy(student);
return requests.map(mapToStudentRequestDTO);
}
export async function getJoinRequestByStudentClass(username: string, classId: string): Promise<ClassJoinRequestDTO> {
const requestRepo = getClassJoinRequestRepository();
const student = await fetchStudent(username);
const cls = await fetchClass(classId);
const request = await requestRepo.findByStudentAndClass(student, cls);
if (!request) {
throw new NotFoundException('Join request not found');
}
return mapToStudentRequestDTO(request);
}
export async function deleteClassJoinRequest(username: string, classId: string): Promise<ClassJoinRequestDTO> {
const requestRepo = getClassJoinRequestRepository();
const student = await fetchStudent(username);
const cls = await fetchClass(classId);
const request = await requestRepo.findByStudentAndClass(student, cls);
if (!request) {
throw new NotFoundException('Join request not found');
}
await requestRepo.deleteBy(student, cls);
return mapToStudentRequestDTO(request);
}

View file

@ -1,8 +1,9 @@
import { getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { Language } from '../entities/content/language.js';
import { getSubmissionRepository } from '../data/repositories.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js';
import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission';
import { Language } from '@dwengo-1/common/util/language';
export async function getSubmission(
loId: LearningObjectIdentifier,
@ -27,24 +28,24 @@ export async function getAllSubmissions(
return submissions.map(mapToSubmissionDTO);
}
export async function createSubmission(submissionDTO: SubmissionDTO) {
export async function createSubmission(submissionDTO: SubmissionDTO): Promise<SubmissionDTO | null> {
const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO);
try {
const newSubmission = await submissionRepository.create(submission);
const newSubmission = submissionRepository.create(submission);
await submissionRepository.save(newSubmission);
} catch (e) {
} catch (_) {
return null;
}
return mapToSubmissionDTO(submission);
}
export async function deleteSubmission(loId: LearningObjectIdentifier, submissionNumber: number) {
export async function deleteSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise<SubmissionDTO> {
const submissionRepository = getSubmissionRepository();
const submission = getSubmission(loId, submissionNumber);
const submission = await getSubmission(loId, submissionNumber);
if (!submission) {
throw new NotFoundException('Could not delete submission because it does not exist');

View file

@ -1,127 +1,167 @@
import { getClassRepository, getLearningObjectRepository, getQuestionRepository, getTeacherRepository } from '../data/repositories.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { getClassStudents } from './classes.js';
import { StudentDTO } from '../interfaces/student.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js';
import {
getClassJoinRequestRepository,
getClassRepository,
getLearningObjectRepository,
getQuestionRepository,
getTeacherRepository,
} from '../data/repositories.js';
import { mapToClassDTO } from '../interfaces/class.js';
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
import { Teacher } from '../entities/users/teacher.entity.js';
import { fetchStudent } from './students.js';
import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js';
import { mapToStudentRequestDTO } from '../interfaces/student-request.js';
import { TeacherRepository } from '../data/users/teacher-repository.js';
import { ClassRepository } from '../data/classes/class-repository.js';
import { Class } from '../entities/classes/class.entity.js';
import { LearningObjectRepository } from '../data/content/learning-object-repository.js';
import { LearningObject } from '../entities/content/learning-object.entity.js';
import { QuestionRepository } from '../data/questions/question-repository.js';
import { Question } from '../entities/questions/question.entity.js';
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
import { Student } from '../entities/users/student.entity.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import {getClassStudents, getClassStudentsDTO} from './classes.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request';
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
const teacherRepository = getTeacherRepository();
const teachers = await teacherRepository.findAll();
const teacherRepository: TeacherRepository = getTeacherRepository();
const users: Teacher[] = await teacherRepository.findAll();
if (full) {
return teachers.map(mapToTeacherDTO);
return users.map(mapToTeacherDTO);
}
return users.map((user) => user.username);
}
export async function fetchTeacher(username: string): Promise<Teacher> {
const teacherRepository: TeacherRepository = getTeacherRepository();
const user: Teacher | null = await teacherRepository.findByUsername(username);
if (!user) {
throw new NotFoundException('Teacher with username not found');
}
return teachers.map((teacher) => teacher.username);
return user;
}
export async function getTeacher(username: string): Promise<TeacherDTO | null> {
const teacherRepository = getTeacherRepository();
const user = await teacherRepository.findByUsername(username);
return user ? mapToTeacherDTO(user) : null;
export async function getTeacher(username: string): Promise<TeacherDTO> {
const user: Teacher = await fetchTeacher(username);
return mapToTeacherDTO(user);
}
export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> {
const teacherRepository = getTeacherRepository();
export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO> {
const teacherRepository: TeacherRepository = getTeacherRepository();
const newTeacher = mapToTeacher(userData);
await teacherRepository.save(newTeacher, { preventOverwrite: true });
return mapToTeacherDTO(newTeacher);
}
export async function deleteTeacher(username: string): Promise<TeacherDTO | null> {
const teacherRepository = getTeacherRepository();
export async function deleteTeacher(username: string): Promise<TeacherDTO> {
const teacherRepository: TeacherRepository = getTeacherRepository();
const user = await teacherRepository.findByUsername(username);
const teacher = await fetchTeacher(username); // Throws error if it does not exist
if (!user) {
return null;
}
try {
await teacherRepository.deleteByUsername(username);
return mapToTeacherDTO(user);
} catch (e) {
console.log(e);
return null;
}
await teacherRepository.deleteByUsername(username);
return mapToTeacherDTO(teacher);
}
export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[] | null> {
const teacherRepository = getTeacherRepository();
const teacher = await teacherRepository.findByUsername(username);
if (!teacher) {
return null;
}
async function fetchClassesByTeacher(username: string): Promise<ClassDTO[]> {
const teacher: Teacher = await fetchTeacher(username);
const classRepository = getClassRepository();
const classes = await classRepository.findByTeacher(teacher);
const classRepository: ClassRepository = getClassRepository();
const classes: Class[] = await classRepository.findByTeacher(teacher);
return classes.map(mapToClassDTO);
}
export async function getClassesByTeacher(username: string, full: boolean): Promise<ClassDTO[] | string[] | null> {
const classes = await fetchClassesByTeacher(username);
if (!classes) {
return null;
}
export async function getClassesByTeacher(username: string, full: boolean): Promise<ClassDTO[] | string[]> {
const classes: ClassDTO[] = await fetchClassesByTeacher(username);
if (full) {
return classes;
}
return classes.map((cls) => cls.id);
}
export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[] | null> {
const classes = (await getClassesByTeacher(username, false)) as string[];
export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[]> {
const classes: ClassDTO[] = await fetchClassesByTeacher(username);
if (!classes) {
return null;
if (!classes || classes.length === 0) {
return [];
}
// workaround
let students;
const classIds: string[] = classes.map((cls) => cls.id);
const students: StudentDTO[] = (await Promise.all(classIds.map(async (username) => await getClassStudentsDTO(username)))).flat();
if (full) {
students = (await Promise.all(classes.map(async (id) => await getClassStudents(id, full) as StudentDTO[]))).flat();
} else {
students = (await Promise.all(classes.map(async (id) => await getClassStudents(id, full) as string[]))).flat();
return students
}
return students;
return students.map((student) => student.username);
}
export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[] | null> {
const teacherRepository = getTeacherRepository();
const teacher = await teacherRepository.findByUsername(username);
if (!teacher) {
return null;
}
export async function getTeacherQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const teacher: Teacher = await fetchTeacher(username);
// Find all learning objects that this teacher manages
const learningObjectRepository = getLearningObjectRepository();
const learningObjects = await learningObjectRepository.findAllByTeacher(teacher);
const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository();
const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher);
if (!learningObjects || learningObjects.length === 0) {
return [];
}
// Fetch all questions related to these learning objects
const questionRepository = getQuestionRepository();
const questions = await questionRepository.findAllByLearningObjects(learningObjects);
return questions.map(mapToQuestionDTO);
}
export async function getQuestionsByTeacher(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[] | null> {
const questions = await fetchTeacherQuestions(username);
if (!questions) {
return null;
}
const questionRepository: QuestionRepository = getQuestionRepository();
const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects);
if (full) {
return questions;
return questions.map(mapToQuestionDTO);
}
return questions.map(mapToQuestionId);
return questions.map(mapToQuestionDTOId);
}
export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> {
const classRepository: ClassRepository = getClassRepository();
const cls: Class | null = await classRepository.findById(classId);
if (!cls) {
throw new NotFoundException('Class with id not found');
}
const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository();
const requests: ClassJoinRequest[] = await requestRepo.findAllOpenRequestsTo(cls);
return requests.map(mapToStudentRequestDTO);
}
export async function updateClassJoinRequestStatus(studentUsername: string, classId: string, accepted = true): Promise<ClassJoinRequestDTO> {
const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository();
const classRepo: ClassRepository = getClassRepository();
const student: Student = await fetchStudent(studentUsername);
const cls: Class | null = await classRepo.findById(classId);
if (!cls) {
throw new NotFoundException('Class not found');
}
const request: ClassJoinRequest | null = await requestRepo.findByStudentAndClass(student, cls);
if (!request) {
throw new NotFoundException('Join request not found');
}
request.status = accepted ? ClassJoinRequestStatus.Accepted : ClassJoinRequestStatus.Declined;
await requestRepo.save(request);
return mapToStudentRequestDTO(request);
}