# Conflicts:
#	backend/src/controllers/questions.ts
#	backend/src/controllers/submissions.ts
#	backend/src/data/questions/question-repository.ts
#	backend/src/interfaces/group.ts
#	backend/src/interfaces/question.ts
#	backend/src/interfaces/submission.ts
#	backend/src/routes/submissions.ts
#	backend/src/services/groups.ts
#	backend/src/services/questions.ts
#	backend/src/services/students.ts
#	backend/src/services/submissions.ts
#	common/src/interfaces/question.ts
This commit is contained in:
Gerald Schmittinger 2025-04-09 20:25:30 +02:00
commit d6dd7fb3bf
90 changed files with 2934 additions and 792 deletions

View file

@ -0,0 +1,70 @@
import { getAnswerRepository } from '../data/repositories.js';
import { Answer } from '../entities/questions/answer.entity.js';
import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js';
import { fetchTeacher } from './teachers.js';
import { fetchQuestion } from './questions.js';
import { QuestionId } from '@dwengo-1/common/interfaces/question';
import { AnswerData, AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
import { NotFoundException } from '../exceptions/not-found-exception.js';
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise<AnswerDTO[] | AnswerId[]> {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question);
if (full) {
return answers.map(mapToAnswerDTO);
}
return answers.map(mapToAnswerDTOId);
}
export async function createAnswer(questionId: QuestionId, answerData: AnswerData): Promise<AnswerDTO> {
const answerRepository = getAnswerRepository();
const toQuestion = await fetchQuestion(questionId);
const author = await fetchTeacher(answerData.author);
const content = answerData.content;
const answer = await answerRepository.createAnswer({
toQuestion,
author,
content,
});
return mapToAnswerDTO(answer);
}
async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
const answer = await answerRepository.findAnswer(question, sequenceNumber);
if (!answer) {
throw new NotFoundException('Answer with questionID and sequence number not found');
}
return answer;
}
export async function getAnswer(questionId: QuestionId, sequenceNumber: number): Promise<AnswerDTO> {
const answer = await fetchAnswer(questionId, sequenceNumber);
return mapToAnswerDTO(answer);
}
export async function deleteAnswer(questionId: QuestionId, sequenceNumber: number): Promise<AnswerDTO> {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
const answer = await fetchAnswer(questionId, sequenceNumber);
await answerRepository.removeAnswerByQuestionAndSequenceNumber(question, sequenceNumber);
return mapToAnswerDTO(answer);
}
export async function updateAnswer(questionId: QuestionId, sequenceNumber: number, answerData: AnswerData): Promise<AnswerDTO> {
const answerRepository = getAnswerRepository();
const answer = await fetchAnswer(questionId, sequenceNumber);
const newAnswer = await answerRepository.updateContent(answer, answerData.content);
return mapToAnswerDTO(newAnswer);
}

View file

@ -1,18 +1,43 @@
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
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 { 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';
import { getLogger } from '../logging/initalize.js';
import { EntityDTO } from '@mikro-orm/core';
import { putObject } from './service-helper.js';
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
export async function fetchAssignment(classid: string, assignmentNumber: number): Promise<Assignment> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
if (!cls) {
return [];
throw new NotFoundException("Could not find assignment's class");
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
throw new NotFoundException('Could not find assignment');
}
return assignment;
}
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
const cls = await fetchClass(classid);
const assignmentRepository = getAssignmentRepository();
const assignments = await assignmentRepository.findAllAssignmentsInClass(cls);
@ -23,42 +48,37 @@ 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();
const newAssignment = assignmentRepository.create(assignment);
await assignmentRepository.save(newAssignment, { preventOverwrite: true });
try {
const newAssignment = assignmentRepository.create(assignment);
await assignmentRepository.save(newAssignment);
return mapToAssignmentDTO(newAssignment);
} catch (e) {
getLogger().error(e);
return null;
}
return mapToAssignmentDTO(newAssignment);
}
export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO | null> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO> {
const assignment = await fetchAssignment(classid, id);
return mapToAssignmentDTO(assignment);
}
if (!cls) {
return null;
}
export async function putAssignment(classid: string, id: number, assignmentData: Partial<EntityDTO<Assignment>>): Promise<AssignmentDTO> {
const assignment = await fetchAssignment(classid, id);
await putObject<Assignment>(assignment, assignmentData, getAssignmentRepository());
return mapToAssignmentDTO(assignment);
}
export async function deleteAssignment(classid: string, id: number): Promise<AssignmentDTO> {
const assignment = await fetchAssignment(classid, id);
const cls = await fetchClass(classid);
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, id);
if (!assignment) {
return null;
}
await assignmentRepository.deleteByClassAndId(cls, id);
return mapToAssignmentDTO(assignment);
}
@ -68,19 +88,7 @@ export async function getAssignmentsSubmissions(
assignmentNumber: number,
full: boolean
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
if (!cls) {
return [];
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return [];
}
const assignment = await fetchAssignment(classid, assignmentNumber);
const groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
@ -94,3 +102,16 @@ export async function getAssignmentsSubmissions(
return submissions.map(mapToSubmissionDTOId);
}
export async function getAssignmentsQuestions(classid: string, assignmentNumber: number, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const assignment = await fetchAssignment(classid, assignmentNumber);
const questionRepository = getQuestionRepository();
const questions = await questionRepository.findAllByAssignment(assignment);
if (full) {
return questions.map(mapToQuestionDTO);
}
return questions.map(mapToQuestionDTO);
}

View file

@ -1,22 +1,25 @@
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
import { getClassRepository, getTeacherInvitationRepository } from '../data/repositories.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';
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.js';
import { fetchStudent } from './students.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { mapToTeacherDTO } from '../interfaces/teacher.js';
import { EntityDTO } from '@mikro-orm/core';
import { putObject } from './service-helper.js';
const logger = getLogger();
export async function fetchClass(classId: string): Promise<Class> {
export async function fetchClass(classid: string): Promise<Class> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
const cls = await classRepository.findById(classid);
if (!cls) {
throw new NotFoundException('Class with id not found');
throw new NotFoundException('Class not found');
}
return cls;
@ -24,11 +27,7 @@ export async function fetchClass(classId: string): Promise<Class> {
export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[]> {
const classRepository = getClassRepository();
const classes = await classRepository.find({}, { populate: ['students', 'teachers'] });
if (!classes) {
return [];
}
const classes = await classRepository.findAll({ populate: ['students', 'teachers'] });
if (full) {
return classes.map(mapToClassDTO);
@ -36,74 +35,71 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[
return classes.map((cls) => cls.classId!);
}
export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> {
const teacherRepository = getTeacherRepository();
const teacherUsernames = classData.teachers || [];
const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter(
(teacher) => teacher !== null
);
const studentRepository = getStudentRepository();
const studentUsernames = classData.students || [];
const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student !== null
);
const classRepository = getClassRepository();
try {
const newClass = classRepository.create({
displayName: classData.displayName,
teachers: teachers,
students: students,
});
await classRepository.save(newClass);
return mapToClassDTO(newClass);
} catch (e) {
logger.error(e);
return null;
}
export async function getClass(classId: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
return mapToClassDTO(cls);
}
export async function getClass(classId: string): Promise<ClassDTO | null> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
export async function createClass(classData: ClassDTO): Promise<ClassDTO> {
const teacherUsernames = classData.teachers || [];
const teachers = await Promise.all(teacherUsernames.map(async (id) => fetchTeacher(id)));
if (!cls) {
return null;
}
const studentUsernames = classData.students || [];
const students = await Promise.all(studentUsernames.map(async (id) => fetchStudent(id)));
const classRepository = getClassRepository();
const newClass = classRepository.create({
displayName: classData.displayName,
teachers: teachers,
students: students,
});
await classRepository.save(newClass, { preventOverwrite: true });
return mapToClassDTO(newClass);
}
export async function putClass(classId: string, classData: Partial<EntityDTO<Class>>): Promise<ClassDTO> {
const cls = await fetchClass(classId);
await putObject<Class>(cls, classData, getClassRepository());
return mapToClassDTO(cls);
}
async function fetchClassStudents(classId: string): Promise<StudentDTO[]> {
export async function deleteClass(classId: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
await classRepository.deleteById(classId);
if (!cls) {
return [];
return mapToClassDTO(cls);
}
export async function getClassStudents(classId: string, full: boolean): Promise<StudentDTO[] | string[]> {
const cls = await fetchClass(classId);
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 getClassStudents(classId: string): Promise<StudentDTO[]> {
return await fetchClassStudents(classId);
}
export async function getClassTeachers(classId: string, full: boolean): Promise<TeacherDTO[] | string[]> {
const cls = await fetchClass(classId);
export async function getClassStudentsIds(classId: string): Promise<string[]> {
const students: StudentDTO[] = await fetchClassStudents(classId);
return students.map((student) => student.username);
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 classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return [];
}
const cls = await fetchClass(classId);
const teacherInvitationRepository = getTeacherInvitationRepository();
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(cls);
@ -114,3 +110,41 @@ export async function getClassTeacherInvitations(classId: string, full: boolean)
return invitations.map(mapToTeacherInvitationDTOIds);
}
export async function deleteClassStudent(classId: string, username: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const newStudents = { students: cls.students.filter((student) => student.username !== username) };
await putObject<Class>(cls, newStudents, getClassRepository());
return mapToClassDTO(cls);
}
export async function deleteClassTeacher(classId: string, username: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const newTeachers = { teachers: cls.teachers.filter((teacher) => teacher.username !== username) };
await putObject<Class>(cls, newTeachers, getClassRepository());
return mapToClassDTO(cls);
}
export async function addClassStudent(classId: string, username: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const newStudent = await fetchStudent(username);
const newStudents = { students: [...cls.students, newStudent] };
await putObject<Class>(cls, newStudents, getClassRepository());
return mapToClassDTO(cls);
}
export async function addClassTeacher(classId: string, username: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const newTeacher = await fetchTeacher(username);
const newTeachers = { teachers: [...cls.teachers, newTeacher] };
await putObject<Class>(cls, newTeachers, getClassRepository());
return mapToClassDTO(cls);
}

View file

@ -1,105 +1,90 @@
import {
getAssignmentRepository,
getClassRepository,
getGroupRepository,
getStudentRepository,
getSubmissionRepository,
} from '../data/repositories.js';
import { EntityDTO } from '@mikro-orm/core';
import { getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js';
import { Group } from '../entities/assignments/group.entity.js';
import { mapToGroupDTO, mapToShallowGroupDTO } 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 { NotFoundException } from '../exceptions/not-found-exception.js';
import { putObject } from './service-helper.js';
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return null;
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return null;
}
export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<Group> {
const assignment = await fetchAssignment(classId, assignmentNumber);
const groupRepository = getGroupRepository();
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber);
if (!group) {
return null;
throw new NotFoundException('Could not find group');
}
if (full) {
return mapToGroupDTO(group);
}
return mapToShallowGroupDTO(group);
return group;
}
export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise<Group | null> {
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> {
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
return mapToGroupDTO(group);
}
export async function putGroup(
classId: string,
assignmentNumber: number,
groupNumber: number,
groupData: Partial<EntityDTO<Group>>
): Promise<GroupDTO> {
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
await putObject<Group>(group, groupData, getGroupRepository());
return mapToGroupDTO(group);
}
export async function deleteGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> {
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
const assignment = await fetchAssignment(classId, assignmentNumber);
const groupRepository = getGroupRepository();
await groupRepository.deleteByAssignmentAndGroupNumber(assignment, groupNumber);
return mapToGroupDTO(group);
}
export async function getExistingGroupFromGroupDTO(groupData: GroupDTO): Promise<Group> {
const classId = typeof groupData.class === 'string' ? groupData.class : groupData.class.id;
const assignmentNumber = typeof groupData.assignment === 'number' ? groupData.assignment : groupData.assignment.id;
const groupNumber = groupData.groupNumber;
return await fetchGroup(classId, assignmentNumber, groupNumber);
}
export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise<GroupDTO> {
const studentRepository = getStudentRepository();
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
const memberUsernames = (groupData.members as string[]) || [];
const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student !== null
);
getLogger().debug(members);
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
if (!cls) {
return null;
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return null;
}
const assignment = await fetchAssignment(classid, assignmentNumber);
const groupRepository = getGroupRepository();
try {
const newGroup = groupRepository.create({
assignment: assignment,
members: members,
});
await groupRepository.save(newGroup);
const newGroup = groupRepository.create({
assignment: assignment,
members: members,
});
await groupRepository.save(newGroup);
return newGroup;
} catch (e) {
getLogger().error(e);
return null;
}
return mapToGroupDTO(newGroup);
}
export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise<GroupDTO[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return [];
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return [];
}
const assignment = await fetchAssignment(classId, assignmentNumber);
const groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
if (full) {
getLogger().debug({ full: full, groups: groups });
return groups.map(mapToGroupDTO);
}
@ -112,26 +97,7 @@ export async function getGroupSubmissions(
groupNumber: number,
full: boolean
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return [];
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return [];
}
const groupRepository = getGroupRepository();
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber);
if (!group) {
return [];
}
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForGroup(group);

View file

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

View file

@ -6,7 +6,7 @@ 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';
import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
const logger: Logger = getLogger();
@ -40,7 +40,7 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
};
}
async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> {
async function findLearningObjectEntityById(id: LearningObjectIdentifierDTO): Promise<LearningObject | null> {
const learningObjectRepo = getLearningObjectRepository();
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
@ -53,7 +53,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
/**
* Fetches a single learning object by its HRUID
*/
async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> {
async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> {
const learningObject = await findLearningObjectEntityById(id);
return convertLearningObject(learningObject);
},
@ -61,7 +61,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
/**
* Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> {
const learningObjectRepo = getLearningObjectRepository();
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);

View file

@ -5,7 +5,7 @@ import { LearningObjectProvider } from './learning-object-provider.js';
import { getLogger, Logger } from '../../logging/initalize.js';
import {
FilteredLearningObject,
LearningObjectIdentifier,
LearningObjectIdentifierDTO,
LearningObjectMetadata,
LearningObjectNode,
LearningPathIdentifier,
@ -67,7 +67,7 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full
const objects = await Promise.all(
nodes.map(async (node) => {
const learningObjectId: LearningObjectIdentifier = {
const learningObjectId: LearningObjectIdentifierDTO = {
hruid: node.learningobject_hruid,
language: learningPathId.language,
};
@ -85,7 +85,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
/**
* Fetches a single learning object by its HRUID
*/
async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> {
async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> {
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`;
const metadata = await fetchWithLogging<LearningObjectMetadata>(
metadataUrl,
@ -121,7 +121,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
* Obtain a HTML-rendering of the learning object with the given identifier (as a string). For learning objects
* from the Dwengo API, this means passing through the HTML rendering from there.
*/
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
async getLearningObjectHTML(id: LearningObjectIdentifierDTO): 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 },

View file

@ -1,10 +1,10 @@
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
export interface LearningObjectProvider {
/**
* Fetches a single learning object by its HRUID
*/
getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null>;
getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null>;
/**
* Fetch full learning object data (metadata)
@ -19,5 +19,5 @@ export interface LearningObjectProvider {
/**
* Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/
getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null>;
getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null>;
}

View file

@ -2,9 +2,9 @@ import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provid
import { LearningObjectProvider } from './learning-object-provider.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';
import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
function getProvider(id: LearningObjectIdentifier): LearningObjectProvider {
function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider {
if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
return databaseLearningObjectProvider;
}
@ -18,7 +18,7 @@ const learningObjectService = {
/**
* Fetches a single learning object by its HRUID
*/
async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> {
async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> {
return getProvider(id).getLearningObjectById(id);
},
@ -39,7 +39,7 @@ const learningObjectService = {
/**
* Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> {
return getProvider(id).getLearningObjectHTML(id);
},
};

View file

@ -12,7 +12,7 @@ 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 { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language';
const prefixes = {
@ -25,7 +25,7 @@ const prefixes = {
blockly: '@blockly',
};
function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifier {
function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifierDTO {
const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split('/');
return {
hruid,

View file

@ -14,7 +14,7 @@ import { LearningObject } from '../../../entities/content/learning-object.entity
import Processor from './processor.js';
import { DwengoContentType } from './content-type.js';
import { replaceAsync } from '../../../util/async.js';
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import { LearningObjectIdentifierDTO } 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;
@ -50,7 +50,7 @@ class ProcessingService {
*/
async render(
learningObject: LearningObject,
fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifier) => Promise<LearningObject | null>
fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifierDTO) => Promise<LearningObject | null>
): Promise<string> {
const html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject);
if (fetchEmbeddedLearningObjects) {

View file

@ -1,15 +1,17 @@
import { getAnswerRepository, getAssignmentRepository, getClassRepository, getGroupRepository, getQuestionRepository } from '../data/repositories.js';
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
import {mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId} from '../interfaces/question.js';
import { Question } from '../entities/questions/question.entity.js';
import { Answer } from '../entities/questions/answer.entity.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 { mapToStudent } from '../interfaces/student.js';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import {QuestionData, QuestionDTO, QuestionId} from '@dwengo-1/common/interfaces/question';
import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import { mapToAssignment } from '../interfaces/assignment.js';
import {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment";
import { fetchStudent } from './students.js';
import {NotFoundException} from "../exceptions/not-found-exception";
import { FALLBACK_VERSION_NUM } from '../config.js';
export async function getQuestionsAboutLearningObjectInAssignment(
loId: LearningObjectIdentifier,
@ -32,10 +34,6 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea
const questionRepository: QuestionRepository = getQuestionRepository();
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
if (!questions) {
return [];
}
if (full) {
return questions.map(mapToQuestionDTO);
}
@ -43,24 +41,22 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea
return questions.map(mapToQuestionDTOId);
}
async function fetchQuestion(questionId: QuestionId): Promise<Question | null> {
export async function fetchQuestion(questionId: QuestionId): Promise<Question> {
const questionRepository = getQuestionRepository();
return await questionRepository.findOne({
learningObjectHruid: questionId.learningObjectIdentifier.hruid,
learningObjectLanguage: questionId.learningObjectIdentifier.language,
learningObjectVersion: questionId.learningObjectIdentifier.version,
sequenceNumber: questionId.sequenceNumber,
});
}
export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO | null> {
const question = await fetchQuestion(questionId);
const question = await questionRepository.findByLearningObjectAndSequenceNumber(
mapToLearningObjectID(questionId.learningObjectIdentifier),
questionId.sequenceNumber
);
if (!question) {
return null;
throw new NotFoundException('Question with loID and sequence number not found');
}
return question;
}
export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO> {
const question = await fetchQuestion(questionId);
return mapToQuestionDTO(question);
}
@ -85,53 +81,43 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean
return answers.map(mapToAnswerDTOId);
}
export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> {
export async function createQuestion(loId: LearningObjectIdentifier, questionData: QuestionData): Promise<QuestionDTO> {
const questionRepository = getQuestionRepository();
const author = await fetchStudent(questionData.author!);
const content = questionData.content;
const author = mapToStudent(questionDTO.author);
const clazz = await getClassRepository().findById((questionData.inGroup.assignment as AssignmentDTO).within);
const assignment = mapToAssignment(questionData.inGroup.assignment as AssignmentDTO, clazz!);
const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber);
const loId: LearningObjectIdentifier = {
...questionDTO.learningObjectIdentifier,
version: questionDTO.learningObjectIdentifier.version ?? 1,
};
const clazz = await getClassRepository().findById((questionDTO.inGroup.assignment as AssignmentDTO).class);
const assignment = mapToAssignment(questionDTO.inGroup.assignment as AssignmentDTO, clazz!);
const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionDTO.inGroup.groupNumber);
try {
await questionRepository.createQuestion({
loId,
author,
inGroup: inGroup!,
content: questionDTO.content,
});
} catch (_) {
return null;
}
return questionDTO;
}
export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO | null> {
const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId);
if (!question) {
return null;
}
const loId: LearningObjectIdentifier = {
...questionId.learningObjectIdentifier,
version: questionId.learningObjectIdentifier.version ?? 1,
};
try {
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(loId, questionId.sequenceNumber);
} catch (_) {
return null;
}
const question = await questionRepository.createQuestion({
loId,
author,
inGroup: inGroup!,
content,
});
return mapToQuestionDTO(question);
}
export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO> {
const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId); // Throws error if not found
const loId: LearningObjectIdentifier = {
hruid: questionId.learningObjectIdentifier.hruid,
language: questionId.learningObjectIdentifier.language,
version: questionId.learningObjectIdentifier.version || FALLBACK_VERSION_NUM,
};
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(loId, questionId.sequenceNumber);
return mapToQuestionDTO(question);
}
export async function updateQuestion(questionId: QuestionId, questionData: QuestionData): Promise<QuestionDTO> {
const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId);
const newQuestion = await questionRepository.updateContent(question, questionData.content);
return mapToQuestionDTO(newQuestion);
}

View file

@ -0,0 +1,20 @@
import { EntityDTO, FromEntityType } from '@mikro-orm/core';
import { DwengoEntityRepository } from '../data/dwengo-entity-repository';
/**
* Utility function to perform an PUT on an object.
*
* @param object The object that needs to be changed
* @param data The datafields and their values that will be updated
* @param repo The repository on which this action needs to be performed
*
* @returns Nothing.
*/
export async function putObject<T extends object>(
object: T,
data: Partial<EntityDTO<FromEntityType<T>>>,
repo: DwengoEntityRepository<T>
): Promise<void> {
repo.assign(object, data);
await repo.getEntityManager().flush();
}

View file

@ -23,6 +23,7 @@ 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';
import { ConflictException } from '../exceptions/conflict-exception.js';
import { Submission } from '../entities/assignments/submission.entity';
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
@ -137,6 +138,10 @@ export async function createClassJoinRequest(username: string, classId: string):
const student = await fetchStudent(username); // Throws error if student not found
const cls = await fetchClass(classId);
if (cls.students.contains(student)) {
throw new ConflictException('Student already in this class');
}
const request = mapToStudentRequest(student, cls);
await requestRepo.save(request, { preventOverwrite: true });
return mapToStudentRequestDTO(request);

View file

@ -1,61 +1,56 @@
import { getAssignmentRepository, 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 } from '../interfaces/submission.js';
import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission';
import { Language } from '@dwengo-1/common/util/language';
export async function getSubmission(
learningObjectHruid: string,
language: Language,
version: number,
submissionNumber: number
): Promise<SubmissionDTO | null> {
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version);
import { fetchStudent } from './students.js';
import { getExistingGroupFromGroupDTO } from './groups.js';
import { Submission } from '../entities/assignments/submission.entity.js';
import {Language} from "@dwengo-1/common/util/language";
export async function fetchSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission> {
const submissionRepository = getSubmissionRepository();
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber);
if (!submission) {
return null;
throw new NotFoundException('Could not find submission');
}
return mapToSubmissionDTO(submission);
}
export async function createSubmission(submissionDTO: SubmissionDTO): Promise<SubmissionDTO | null> {
const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO);
try {
const newSubmission = submissionRepository.create(submission);
await submissionRepository.save(newSubmission);
} catch (_) {
return null;
}
return mapToSubmissionDTO(submission);
}
export async function deleteSubmission(
learningObjectHruid: string,
language: Language,
version: number,
submissionNumber: number
): Promise<SubmissionDTO | null> {
const submissionRepository = getSubmissionRepository();
const submission = getSubmission(learningObjectHruid, language, version, submissionNumber);
if (!submission) {
return null;
}
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version);
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber);
return submission;
}
export async function getSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise<SubmissionDTO> {
const submission = await fetchSubmission(loId, submissionNumber);
return mapToSubmissionDTO(submission);
}
export async function getAllSubmissions(loId: LearningObjectIdentifier): Promise<SubmissionDTO[]> {
const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findByLearningObject(loId);
return submissions.map(mapToSubmissionDTO);
}
export async function createSubmission(submissionDTO: SubmissionDTO): Promise<SubmissionDTO> {
const submitter = await fetchStudent(submissionDTO.submitter.username);
const group = submissionDTO.group ? await getExistingGroupFromGroupDTO(submissionDTO.group) : undefined;
const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO, submitter, group);
await submissionRepository.save(submission);
return mapToSubmissionDTO(submission);
}
export async function deleteSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise<SubmissionDTO> {
const submission = await fetchSubmission(loId, submissionNumber);
const submissionRepository = getSubmissionRepository();
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber);
return mapToSubmissionDTO(submission);
}
/**
* Returns all the submissions made by on behalf of any group the given student is in.
*/

View file

@ -22,13 +22,14 @@ 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 } from './classes.js';
import { addClassStudent, fetchClass, 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';
import { ConflictException } from '../exceptions/conflict-exception.js';
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
const teacherRepository: TeacherRepository = getTeacherRepository();
@ -99,10 +100,12 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
const classIds: string[] = classes.map((cls) => cls.id);
const students: StudentDTO[] = (await Promise.all(classIds.map(async (id) => getClassStudents(id)))).flat();
const students: StudentDTO[] = (await Promise.all(classIds.map(async (username) => await getClassStudentsDTO(username)))).flat();
if (full) {
return students;
}
return students.map((student) => student.username);
}
@ -143,13 +146,12 @@ export async function getJoinRequestsByClass(classId: string): Promise<ClassJoin
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);
const cls = await fetchClass(classId);
if (!cls) {
throw new NotFoundException('Class not found');
if (cls.students.contains(student)) {
throw new ConflictException('Student already in this class');
}
const request: ClassJoinRequest | null = await requestRepo.findByStudentAndClass(student, cls);
@ -158,8 +160,14 @@ export async function updateClassJoinRequestStatus(studentUsername: string, clas
throw new NotFoundException('Join request not found');
}
request.status = accepted ? ClassJoinRequestStatus.Accepted : ClassJoinRequestStatus.Declined;
request.status = ClassJoinRequestStatus.Declined;
if (accepted) {
request.status = ClassJoinRequestStatus.Accepted;
await addClassStudent(classId, studentUsername);
}
await requestRepo.save(request);
return mapToStudentRequestDTO(request);
}