feat(backend): SearchByAdmins service
This commit is contained in:
parent
0d2b486a2c
commit
1639fbdabf
7 changed files with 99 additions and 17 deletions
|
@ -50,6 +50,15 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
|
|||
return;
|
||||
} else {
|
||||
hruidList = themes.flatMap((theme) => theme.hruids);
|
||||
const apiLearningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, 'All themes', forGroup);
|
||||
// TODO Remove hardcoding
|
||||
const userLearningPaths = await learningPathService.searchLearningPathsByAdmin(['testleerkracht1'], language as Language, forGroup);
|
||||
if (!apiLearningPaths.data) {
|
||||
res.json(userLearningPaths);
|
||||
return;
|
||||
}
|
||||
res.json(apiLearningPaths.data.concat(userLearningPaths));
|
||||
return;
|
||||
}
|
||||
|
||||
const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup);
|
||||
|
|
|
@ -1,21 +1,35 @@
|
|||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||
import { LearningPath } from '../../entities/content/learning-path.entity.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { MatchMode } from '@dwengo-1/common/util/match-mode';
|
||||
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
||||
import { RequiredEntityData } from '@mikro-orm/core';
|
||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||
import { EntityAlreadyExistsException } from '../../exceptions/entity-already-exists-exception.js';
|
||||
import { Teacher } from '../../entities/users/teacher.entity';
|
||||
|
||||
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
|
||||
public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
|
||||
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] });
|
||||
}
|
||||
|
||||
public async findByAdmins(admins: Teacher[], language: Language, _matchMode?: MatchMode): Promise<LearningPath[]> {
|
||||
return this.findAll({
|
||||
where: {
|
||||
language: language,
|
||||
admins: {
|
||||
$in: admins,
|
||||
},
|
||||
},
|
||||
populate: ['nodes', 'nodes.transitions'],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all learning paths which have the given language and whose title OR description contains the
|
||||
* query string.
|
||||
*
|
||||
* @param query The query string we want to seach for in the title or description.
|
||||
* @param query The query string we want to search for in the title or description.
|
||||
* @param language The language of the learning paths we want to find.
|
||||
*/
|
||||
public async findByQueryStringAndLanguage(query: string, language: Language): Promise<LearningPath[]> {
|
||||
|
|
|
@ -13,9 +13,11 @@ import {
|
|||
Transition,
|
||||
} from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { MatchMode } from '@dwengo-1/common/util/match-mode';
|
||||
import { Group } from '../../entities/assignments/group.entity';
|
||||
import { Collection } from '@mikro-orm/core';
|
||||
import { v4 } from 'uuid';
|
||||
import { Teacher } from '../../entities/users/teacher.entity';
|
||||
|
||||
/**
|
||||
* Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its
|
||||
|
@ -34,9 +36,9 @@ async function getLearningObjectsForNodes(nodes: Collection<LearningPathNode>):
|
|||
version: node.version,
|
||||
language: node.language,
|
||||
})
|
||||
.then((learningObject) => [node, learningObject] as [LearningPathNode, FilteredLearningObject | null])
|
||||
)
|
||||
)
|
||||
.then((learningObject) => [node, learningObject] as [LearningPathNode, FilteredLearningObject | null]),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (Array.from(nullableNodesToLearningObjects.values()).some((it) => it === null)) {
|
||||
throw new Error('At least one of the learning objects on this path could not be found.');
|
||||
|
@ -93,14 +95,14 @@ async function convertNode(
|
|||
node: LearningPathNode,
|
||||
learningObject: FilteredLearningObject,
|
||||
personalizedFor: Group | undefined,
|
||||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>
|
||||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
|
||||
): Promise<LearningObjectNode> {
|
||||
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(node, personalizedFor) : null;
|
||||
const transitions = node.transitions
|
||||
.filter(
|
||||
(trans) =>
|
||||
!personalizedFor || // If we do not want a personalized learning path, keep all transitions
|
||||
isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // Otherwise remove all transitions that aren't possible.
|
||||
isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)), // Otherwise remove all transitions that aren't possible.
|
||||
)
|
||||
.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects));
|
||||
return {
|
||||
|
@ -125,10 +127,10 @@ async function convertNode(
|
|||
*/
|
||||
async function convertNodes(
|
||||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
|
||||
personalizedFor?: Group
|
||||
personalizedFor?: Group,
|
||||
): Promise<LearningObjectNode[]> {
|
||||
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) =>
|
||||
convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects)
|
||||
convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects),
|
||||
);
|
||||
return await Promise.all(nodesPromise);
|
||||
}
|
||||
|
@ -155,7 +157,7 @@ function optionalJsonStringToObject(jsonString?: string): object | null {
|
|||
function convertTransition(
|
||||
transition: LearningPathTransition,
|
||||
index: number,
|
||||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>
|
||||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
|
||||
): Transition {
|
||||
const nextNode = nodesToLearningObjects.get(transition.next);
|
||||
if (!nextNode) {
|
||||
|
@ -185,10 +187,10 @@ const databaseLearningPathProvider: LearningPathProvider = {
|
|||
const learningPathRepo = getLearningPathRepository();
|
||||
|
||||
const learningPaths = (await Promise.all(hruids.map(async (hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter(
|
||||
(learningPath) => learningPath !== null
|
||||
(learningPath) => learningPath !== null,
|
||||
);
|
||||
const filteredLearningPaths = await Promise.all(
|
||||
learningPaths.map(async (learningPath, index) => convertLearningPath(learningPath, index, personalizedFor))
|
||||
learningPaths.map(async (learningPath, index) => convertLearningPath(learningPath, index, personalizedFor)),
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -207,6 +209,13 @@ const databaseLearningPathProvider: LearningPathProvider = {
|
|||
const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language);
|
||||
return await Promise.all(searchResults.map(async (result, index) => convertLearningPath(result, index, personalizedFor)));
|
||||
},
|
||||
|
||||
async searchLearningPathsByAdmin(admins: Teacher[], language: Language, personalizedFor?: Group, matchMode?: MatchMode): Promise<LearningPath[]> {
|
||||
const learningPathRepo = getLearningPathRepository();
|
||||
|
||||
const searchResults = await learningPathRepo.findByAdmins(admins, language, matchMode);
|
||||
return await Promise.all(searchResults.map(async (result, index) => convertLearningPath(result, index, personalizedFor)));
|
||||
},
|
||||
};
|
||||
|
||||
export default databaseLearningPathProvider;
|
||||
|
|
|
@ -3,6 +3,8 @@ import { DWENGO_API_BASE } from '../../config.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';
|
||||
import { Teacher } from '../../entities/users/teacher.entity';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
|
@ -38,6 +40,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
|
|||
data: learningPaths,
|
||||
};
|
||||
},
|
||||
|
||||
async searchLearningPaths(query: string, language: string): Promise<LearningPath[]> {
|
||||
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
|
||||
const params = { all: query, language };
|
||||
|
@ -45,6 +48,15 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
|
|||
const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params });
|
||||
return searchResults ?? [];
|
||||
},
|
||||
|
||||
async searchLearningPathsByAdmin(admins: Teacher[], language: string): Promise<LearningPath[]> {
|
||||
if (!admins || admins.length === 0) {
|
||||
return this.searchLearningPaths('', language as Language);
|
||||
}
|
||||
|
||||
// Dwengo API does not have the concept of admins, so we cannot filter by them.
|
||||
return []
|
||||
},
|
||||
};
|
||||
|
||||
export default dwengoApiLearningPathProvider;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { MatchMode } from '@dwengo-1/common/util/match-mode';
|
||||
import { Group } from '../../entities/assignments/group.entity';
|
||||
import { Teacher } from '../../entities/users/teacher.entity';
|
||||
|
||||
/**
|
||||
* Generic interface for a service which provides access to learning paths from a data source.
|
||||
|
@ -15,4 +17,9 @@ export interface LearningPathProvider {
|
|||
* Search learning paths in the data source using the given search string.
|
||||
*/
|
||||
searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]>;
|
||||
|
||||
/**
|
||||
* Fetch the learning paths for the given admins from the data source.
|
||||
*/
|
||||
searchLearningPathsByAdmin(admins: Teacher[], language: Language, personalizedFor?: Group, matchMode?: MatchMode): Promise<LearningPath[]>;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { LearningObjectNode, LearningPath, LearningPathResponse } from '@dwengo-
|
|||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { Group } from '../../entities/assignments/group.entity.js';
|
||||
import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity.js';
|
||||
import { getLearningPathRepository } from '../../data/repositories.js';
|
||||
import { getLearningPathRepository, getTeacherRepository } from '../../data/repositories.js';
|
||||
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||
import { base64ToArrayBuffer } from '../../util/base64-buffer-conversion.js';
|
||||
|
@ -37,11 +37,11 @@ export function mapToLearningPath(dto: LearningPath, adminsDto: TeacherDTO[]): L
|
|||
startNode: nodeDto.start_node ?? false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
dto.nodes.forEach((nodeDto) => {
|
||||
const fromNode = nodes.find(
|
||||
(it) => it.learningObjectHruid === nodeDto.learningobject_hruid && it.language === nodeDto.language && it.version === nodeDto.version
|
||||
(it) => it.learningObjectHruid === nodeDto.learningobject_hruid && it.language === nodeDto.language && it.version === nodeDto.version,
|
||||
)!;
|
||||
const transitions = nodeDto.transitions
|
||||
.map((transDto, i) => {
|
||||
|
@ -49,7 +49,7 @@ export function mapToLearningPath(dto: LearningPath, adminsDto: TeacherDTO[]): L
|
|||
(it) =>
|
||||
it.learningObjectHruid === transDto.next.hruid &&
|
||||
it.language === transDto.next.language &&
|
||||
it.version === transDto.next.version
|
||||
it.version === transDto.next.version,
|
||||
);
|
||||
|
||||
if (toNode) {
|
||||
|
@ -93,7 +93,7 @@ const learningPathService = {
|
|||
nonUserContentHruids,
|
||||
language,
|
||||
source,
|
||||
personalizedFor
|
||||
personalizedFor,
|
||||
);
|
||||
|
||||
const result = (userContentLearningPaths.data || []).concat(nonUserContentLearningPaths.data || []);
|
||||
|
@ -110,7 +110,28 @@ const learningPathService = {
|
|||
*/
|
||||
async searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]> {
|
||||
const providerResponses = await Promise.all(
|
||||
allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor))
|
||||
allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor)),
|
||||
);
|
||||
return providerResponses.flat();
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the learning paths for the given admins from the data source.
|
||||
*/
|
||||
async searchLearningPathsByAdmin(adminsIds: string[], language: Language, personalizedFor?: Group): Promise<LearningPath[]> {
|
||||
const teacherRepo = getTeacherRepository();
|
||||
const admins = await Promise.all(
|
||||
adminsIds.map(async (adminId) => {
|
||||
const admin = await teacherRepo.findByUsername(adminId);
|
||||
if (!admin) {
|
||||
throw new Error(`Admin with ID ${adminId} not found.`);
|
||||
}
|
||||
return admin;
|
||||
}),
|
||||
);
|
||||
|
||||
const providerResponses = await Promise.all(
|
||||
allProviders.map(async (provider) => provider.searchLearningPathsByAdmin(admins, language, personalizedFor)),
|
||||
);
|
||||
return providerResponses.flat();
|
||||
},
|
||||
|
|
10
common/src/util/match-mode.ts
Normal file
10
common/src/util/match-mode.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export enum MatchMode {
|
||||
/**
|
||||
* Match any
|
||||
*/
|
||||
ANY = 'ANY',
|
||||
/**
|
||||
* Match all
|
||||
*/
|
||||
ALL = 'ALL',
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue