Merge branch 'dev' into feat/232-assignments-pagina-ui-ux
Merge dev into feat/232-assignments-pagina-ui-ux
This commit is contained in:
		
						commit
						96076844a5
					
				
					 56 changed files with 1265 additions and 867 deletions
				
			
		|  | @ -13,7 +13,7 @@ | |||
| #DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api | ||||
| # The default fallback language. | ||||
| #DWENGO_FALLBACK_LANGUAGE=nl | ||||
| # Whether running in production mode or not. Possible values are "prod" or "dev". | ||||
| # Whether running in production mode or not. Possible values are "prod", "staging", "test" or "dev". | ||||
| #DWENGO_RUN_MODE=dev | ||||
| 
 | ||||
| # ! Change this! The hostname or IP address of the database | ||||
|  | @ -66,3 +66,12 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://hostname/realms/teacher/protocol/openid | |||
| # The address of the Loki instance, a log aggregation system. | ||||
| # If running your stack in docker, this should use the docker service name. | ||||
| #DWENGO_LOGGING_LOKI_HOST=http://localhost:3102 | ||||
| 
 | ||||
| # The hostname or IP address of the caching server, e.g. Memcached. | ||||
| # If running your stack in docker, this should use the docker service name. | ||||
| #DWENGO_CACHE_HOST=localhost | ||||
| #DWENGO_CACHE_PORT=11211 | ||||
| # The time-to-live (TTL) for cached items in seconds. | ||||
| #DWENGO_CACHE_TTL=3600 | ||||
| # If your cache server benefits from a prefix, you can set it here. | ||||
| #DWENGO_CACHE_KEY_PREFIX=dwengo | ||||
|  |  | |||
|  | @ -35,3 +35,7 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/op | |||
| 
 | ||||
| DWENGO_LOGGING_LEVEL=info | ||||
| DWENGO_LOGGING_LOKI_HOST=http://logging:3102 | ||||
| 
 | ||||
| DWENGO_CACHE_HOST=caching | ||||
| #DWENGO_CACHE_PORT=11211 | ||||
| DWENGO_CACHE_TTL=604800 | ||||
|  |  | |||
|  | @ -1,4 +1,6 @@ | |||
| PORT=3000 | ||||
| DWENGO_RUN_MODE=staging | ||||
| 
 | ||||
| DWENGO_DB_HOST=db | ||||
| DWENGO_DB_PORT=5432 | ||||
| DWENGO_DB_USERNAME=postgres | ||||
|  | @ -18,4 +20,8 @@ DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080 | |||
| 
 | ||||
| # Logging and monitoring | ||||
| 
 | ||||
| LOKI_HOST=http://logging:3102 | ||||
| DWENGO_LOGGING_LEVEL=debug | ||||
| DWENGO_LOGGING_LOKI_HOST=http://logging:3102 | ||||
| 
 | ||||
| DWENGO_CACHE_HOST=caching | ||||
| DWENGO_CACHE_TTL=86400 | ||||
|  |  | |||
|  | @ -4,6 +4,8 @@ | |||
| # Should not need to be modified. | ||||
| # See .env.example for more information. | ||||
| # | ||||
| ### Advanced configuration ### | ||||
| 
 | ||||
| 
 | ||||
| ### Dwengo ### | ||||
| 
 | ||||
|  | @ -21,3 +23,7 @@ DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | |||
| DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs | ||||
| 
 | ||||
| DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:9876,* | ||||
| 
 | ||||
| ### Advanced configuration ### | ||||
| 
 | ||||
| DWENGO_LOGGING_LEVEL=debug | ||||
|  |  | |||
|  | @ -27,6 +27,13 @@ FROM node:22 AS production-stage | |||
| 
 | ||||
| WORKDIR /app/dwengo | ||||
| 
 | ||||
| COPY package*.json ./ | ||||
| COPY backend/package.json ./backend/ | ||||
| # Backend depends on common | ||||
| COPY common/package.json ./common/ | ||||
| 
 | ||||
| RUN npm install --silent --only=production | ||||
| 
 | ||||
| # Copy static files | ||||
| 
 | ||||
| COPY ./backend/i18n ./i18n | ||||
|  | @ -37,15 +44,6 @@ COPY --from=build-stage /app/dwengo/common/dist ./common/dist | |||
| COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist | ||||
| COPY --from=build-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json | ||||
| 
 | ||||
| COPY package*.json ./ | ||||
| COPY backend/package.json ./backend/ | ||||
| # Backend depends on common | ||||
| COPY common/package.json ./common/ | ||||
| 
 | ||||
| RUN npm install --silent --only=production | ||||
| 
 | ||||
| COPY ./backend/i18n ./backend/i18n | ||||
| 
 | ||||
| EXPOSE 3000 | ||||
| 
 | ||||
| CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"] | ||||
|  |  | |||
|  | @ -38,6 +38,7 @@ | |||
|         "jwks-rsa": "^3.1.0", | ||||
|         "loki-logger-ts": "^1.0.2", | ||||
|         "marked": "^15.0.7", | ||||
|         "memjs": "^1.3.2", | ||||
|         "mime-types": "^3.0.1", | ||||
|         "nanoid": "^5.1.5", | ||||
|         "response-time": "^2.3.3", | ||||
|  | @ -53,6 +54,7 @@ | |||
|         "@types/express": "^5.0.0", | ||||
|         "@types/express-fileupload": "^1.5.1", | ||||
|         "@types/js-yaml": "^4.0.9", | ||||
|         "@types/memjs": "^1.3.3", | ||||
|         "@types/mime-types": "^2.1.4", | ||||
|         "@types/node": "^22.13.4", | ||||
|         "@types/response-time": "^2.3.8", | ||||
|  |  | |||
							
								
								
									
										44
									
								
								backend/src/caching.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								backend/src/caching.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | |||
| import { getLogger } from './logging/initalize.js'; | ||||
| import { envVars, getEnvVar } from './util/envVars.js'; | ||||
| import { Client } from 'memjs'; | ||||
| 
 | ||||
| export type CacheClient = Client; | ||||
| 
 | ||||
| let cacheClient: CacheClient; | ||||
| 
 | ||||
| async function initializeClient(): Promise<CacheClient> { | ||||
|     if (cacheClient !== undefined) { | ||||
|         return cacheClient; | ||||
|     } | ||||
| 
 | ||||
|     const cachingHost = getEnvVar(envVars.CacheHost); | ||||
|     const cachingPort = getEnvVar(envVars.CachePort); | ||||
|     const cachingUrl = `${cachingHost}:${cachingPort}`; | ||||
| 
 | ||||
|     if (cachingHost === '') { | ||||
|         return cacheClient; | ||||
|     } | ||||
| 
 | ||||
|     cacheClient = Client.create(cachingUrl); | ||||
| 
 | ||||
|     getLogger().info(`Memcached client initialized at ${cachingUrl}`); | ||||
| 
 | ||||
|     return cacheClient; | ||||
| } | ||||
| 
 | ||||
| export async function getCacheClient(): Promise<CacheClient> { | ||||
|     cacheClient ||= await initializeClient(); | ||||
|     return cacheClient; | ||||
| } | ||||
| 
 | ||||
| export async function checkCachingHealth(): Promise<boolean> { | ||||
|     try { | ||||
|         const client = await getCacheClient(); | ||||
|         await client.set('health', Buffer.from('ok'), { expires: 60 }); | ||||
|         const reply = await cacheClient.get('health'); | ||||
|         return reply?.value?.toString() === 'ok'; | ||||
|     } catch (error) { | ||||
|         getLogger().error('Caching Health Check Failed:', error); | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | @ -29,7 +29,8 @@ function initializeLogger(): Logger { | |||
|         format: format.combine(format.cli(), format.simple()), | ||||
|     }); | ||||
| 
 | ||||
|     if (getEnvVar(envVars.RunMode) === 'dev') { | ||||
|     const runMode = getEnvVar(envVars.RunMode); | ||||
|     if (runMode === 'dev' || runMode.includes('test')) { | ||||
|         logger = createLogger({ | ||||
|             transports: [consoleTransport], | ||||
|         }); | ||||
|  |  | |||
|  | @ -7,12 +7,17 @@ let orm: MikroORM | undefined; | |||
| export async function initORM(testingMode = false): Promise<MikroORM<IDatabaseDriver, EntityManager>> { | ||||
|     const logger: Logger = getLogger(); | ||||
| 
 | ||||
|     logger.info('Initializing ORM'); | ||||
|     logger.debug('MikroORM config is', config); | ||||
|     const options = config(testingMode); | ||||
| 
 | ||||
|     logger.info('MikroORM config is', options); | ||||
| 
 | ||||
|     logger.info('Initializing ORM'); | ||||
|     orm = await MikroORM.init(options); | ||||
|     logger.info('MikroORM initialized'); | ||||
| 
 | ||||
|     orm = await MikroORM.init(config(testingMode)); | ||||
|     // Update the database scheme if necessary and enabled.
 | ||||
|     if (getEnvVar(envVars.DbUpdate)) { | ||||
|         logger.info('MikroORM: Updating database schema'); | ||||
|         await orm.schema.updateSchema(); | ||||
|     } else { | ||||
|         const diff = await orm.schema.getUpdateSchemaSQL(); | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { DWENGO_API_BASE } from '../config.js'; | ||||
| import { fetchWithLogging } from '../util/api-helper.js'; | ||||
| import { fetchRemote } from '../util/api-helper.js'; | ||||
| 
 | ||||
| import { | ||||
|     FilteredLearningObject, | ||||
|  | @ -39,10 +39,7 @@ function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLear | |||
|  */ | ||||
| export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> { | ||||
|     const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; | ||||
|     const metadata = await fetchWithLogging<LearningObjectMetadata>( | ||||
|         metadataUrl, | ||||
|         `Metadata for Learning Object HRUID "${hruid}" (language ${language})` | ||||
|     ); | ||||
|     const metadata = await fetchRemote<LearningObjectMetadata>(metadataUrl, `Metadata for Learning Object HRUID "${hruid}" (language ${language})`); | ||||
| 
 | ||||
|     if (!metadata) { | ||||
|         getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`); | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { DWENGO_API_BASE } from '../../config.js'; | ||||
| import { fetchWithLogging } from '../../util/api-helper.js'; | ||||
| import { fetchRemote } 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'; | ||||
|  | @ -88,7 +88,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | |||
|      */ | ||||
|     async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> { | ||||
|         const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; | ||||
|         const metadata = await fetchWithLogging<LearningObjectMetadata>( | ||||
|         const metadata = await fetchRemote<LearningObjectMetadata>( | ||||
|             metadataUrl, | ||||
|             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, | ||||
|             { | ||||
|  | @ -124,7 +124,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | |||
|      */ | ||||
|     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})`, { | ||||
|         const html = await fetchRemote<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { | ||||
|             params: { ...id }, | ||||
|         }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { fetchWithLogging } from '../../util/api-helper.js'; | ||||
| import { fetchRemote } from '../../util/api-helper.js'; | ||||
| import { DWENGO_API_BASE } from '../../config.js'; | ||||
| import { LearningPathProvider } from './learning-path-provider.js'; | ||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | ||||
|  | @ -42,7 +42,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | |||
|         const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; | ||||
|         const params = { pathIdList: JSON.stringify({ hruids }), language }; | ||||
| 
 | ||||
|         const learningPaths = await fetchWithLogging<LearningPath[]>(apiUrl, `Learning paths for ${source}`, { params }); | ||||
|         const learningPaths = await fetchRemote<LearningPath[]>(apiUrl, `Learning paths for ${source}`, { params }); | ||||
| 
 | ||||
|         if (!learningPaths || learningPaths.length === 0) { | ||||
|             logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`); | ||||
|  | @ -66,12 +66,11 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | |||
|         const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; | ||||
|         const params = { all: query, language }; | ||||
| 
 | ||||
|         const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params }); | ||||
|         const searchResults = await fetchRemote<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params }); | ||||
| 
 | ||||
|         if (searchResults) { | ||||
|             await Promise.all(searchResults?.map(async (it) => addProgressToLearningPath(it, personalizedFor))); | ||||
|         } | ||||
| 
 | ||||
|         return searchResults ?? []; | ||||
|     }, | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,28 +1,66 @@ | |||
| import axios, { AxiosRequestConfig } from 'axios'; | ||||
| import { getLogger, Logger } from '../logging/initalize.js'; | ||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||
| import { getCacheClient } from '../caching.js'; | ||||
| import { envVars, getEnvVar, getNumericEnvVar } from './envVars.js'; | ||||
| import { createHash } from 'crypto'; | ||||
| 
 | ||||
| const cacheClient = await getCacheClient(); | ||||
| const logger: Logger = getLogger(); | ||||
| const runMode: string = getEnvVar(envVars.RunMode); | ||||
| const prefix: string = getEnvVar(envVars.CacheKeyPrefix); | ||||
| 
 | ||||
| interface Options { | ||||
|     params?: Record<string, unknown> | LearningObjectIdentifier; | ||||
|     query?: Record<string, unknown>; | ||||
|     responseType?: 'json' | 'text'; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Utility function to fetch data from an API endpoint with error handling. | ||||
|  * Utility function to fetch data from an API endpoint with error handling and caching. | ||||
|  * Logs errors but does NOT throw exceptions to keep the system running. | ||||
|  * | ||||
|  * @param url The API endpoint to fetch from. | ||||
|  * @param description A short description of what is being fetched (for logging). | ||||
|  * @param options Contains further options such as params (the query params) and responseType (whether the response | ||||
|  *                should be parsed as JSON ("json") or whether it should be returned as plain text ("text") | ||||
|  * @param cacheTTL Time-to-live for the cache in seconds (default: 60 seconds). | ||||
|  * @returns The response data if successful, or null if an error occurs. | ||||
|  */ | ||||
| export async function fetchWithLogging<T>( | ||||
|     url: string, | ||||
|     description: string, | ||||
|     options?: { | ||||
|         params?: Record<string, unknown> | LearningObjectIdentifier; | ||||
|         query?: Record<string, unknown>; | ||||
|         responseType?: 'json' | 'text'; | ||||
| export async function fetchRemote<T>(url: string, description: string, options?: Options, cacheTTL?: number): Promise<T | null> { | ||||
|     if (runMode !== 'dev' && !runMode.includes('test') && cacheClient !== undefined) { | ||||
|         return fetchWithCache<T>(url, description, options, cacheTTL); | ||||
|     } | ||||
| ): Promise<T | null> { | ||||
| 
 | ||||
|     getLogger().debug(`🔄 INFO: Bypassing cache for ${description} at "${url}".`); | ||||
|     return fetchWithLogging(url, description, options); | ||||
| } | ||||
| 
 | ||||
| async function fetchWithCache<T>(url: string, description: string, options?: Options, cacheTTL?: number): Promise<T | null> { | ||||
|     // Combine the URL and parameters to create a unique cache key.
 | ||||
|     // NOTE Using a hash function to keep the key short, since Memcached has a limit on key size
 | ||||
|     const urlWithParams = `${url}${options?.params ? JSON.stringify(options.params) : ''}`; | ||||
|     const hashedUrl = createHash('sha256').update(urlWithParams).digest('hex'); | ||||
|     const key = `${prefix}:${hashedUrl}`; | ||||
| 
 | ||||
|     const cachedData = await cacheClient.get(key); | ||||
| 
 | ||||
|     if (cachedData?.value) { | ||||
|         logger.debug(`✅ INFO: Cache hit for ${description} at "${url}" (key: "${key}")`); | ||||
|         return JSON.parse(cachedData.value.toString()) as T; | ||||
|     } | ||||
| 
 | ||||
|     logger.debug(`🔄 INFO: Cache miss for ${description} at "${url}". Fetching data...`); | ||||
|     const response = await fetchWithLogging<T>(url, description, options); | ||||
| 
 | ||||
|     const ttl = cacheTTL || getNumericEnvVar(envVars.CacheTTL); | ||||
|     await cacheClient.set(key, JSON.stringify(response), { expires: ttl }); | ||||
|     logger.debug(`✅ INFO: Cached response for ${description} at "${url}" for ${ttl} seconds. (key: "${key}")`); | ||||
| 
 | ||||
|     return response; | ||||
| } | ||||
| 
 | ||||
| async function fetchWithLogging<T>(url: string, description: string, options?: Options): Promise<T | null> { | ||||
|     try { | ||||
|         const config: AxiosRequestConfig = options || {}; | ||||
|         const response = await axios.get<T>(url, config); | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; | |||
| const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; | ||||
| const CORS_PREFIX = PREFIX + 'CORS_'; | ||||
| const LOGGING_PREFIX = PREFIX + 'LOGGING_'; | ||||
| const CACHE_PREFIX = PREFIX + 'CACHE_'; | ||||
| 
 | ||||
| interface EnvVar { | ||||
|     key: string; | ||||
|  | @ -39,6 +40,11 @@ export const envVars: Record<string, EnvVar> = { | |||
| 
 | ||||
|     LogLevel: { key: LOGGING_PREFIX + 'LEVEL', defaultValue: 'info' }, | ||||
|     LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' }, | ||||
| 
 | ||||
|     CacheHost: { key: CACHE_PREFIX + 'HOST' }, | ||||
|     CachePort: { key: CACHE_PREFIX + 'PORT', defaultValue: 11211 }, | ||||
|     CacheTTL: { key: CACHE_PREFIX + 'TTL', defaultValue: 60 * 60 * 24 }, // 24 hours
 | ||||
|     CacheKeyPrefix: { key: CACHE_PREFIX + 'KEY_PREFIX', defaultValue: 'dwengo' }, | ||||
| } as const; | ||||
| 
 | ||||
| /** | ||||
|  | @ -56,7 +62,7 @@ export function getEnvVar(envVar: EnvVar): string { | |||
|     } else if (envVar.required) { | ||||
|         throw new Error(`Missing environment variable: ${envVar.key}`); | ||||
|     } else { | ||||
|         return String(envVar.defaultValue) || ''; | ||||
|         return envVar.defaultValue !== undefined ? String(envVar.defaultValue) || '' : ''; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import { Language } from '@dwengo-1/common/util/language'; | |||
| import { getAllAnswersHandler, getAnswerHandler, updateAnswerHandler } from '../../src/controllers/answers'; | ||||
| import { BadRequestException } from '../../src/exceptions/bad-request-exception'; | ||||
| import { NotFoundException } from '../../src/exceptions/not-found-exception'; | ||||
| import { getAnswer02 } from '../test_assets/questions/answers.testdata'; | ||||
| 
 | ||||
| describe('Questions controllers', () => { | ||||
|     let req: Partial<Request>; | ||||
|  | @ -24,9 +25,14 @@ describe('Questions controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get answers list', async () => { | ||||
|         const a = getAnswer02(); | ||||
|         req = { | ||||
|             params: { hruid: 'id05', version: '1', seq: '2' }, | ||||
|             query: { lang: Language.English, full: 'true' }, | ||||
|             params: { | ||||
|                 hruid: a.toQuestion.learningObjectHruid, | ||||
|                 version: a.toQuestion.learningObjectVersion.toString(), | ||||
|                 seq: a.toQuestion.sequenceNumber!.toString(), | ||||
|             }, | ||||
|             query: { lang: a.toQuestion.learningObjectLanguage, full: 'true' }, | ||||
|         }; | ||||
| 
 | ||||
|         await getAllAnswersHandler(req as Request, res as Response); | ||||
|  | @ -38,9 +44,15 @@ describe('Questions controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get answer', async () => { | ||||
|         const a = getAnswer02(); | ||||
|         req = { | ||||
|             params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2' }, | ||||
|             query: { lang: Language.English, full: 'true' }, | ||||
|             params: { | ||||
|                 hruid: a.toQuestion.learningObjectHruid, | ||||
|                 version: a.toQuestion.learningObjectVersion.toString(), | ||||
|                 seq: a.toQuestion.sequenceNumber!.toString(), | ||||
|                 seqAnswer: a.sequenceNumber!.toString(), | ||||
|             }, | ||||
|             query: { lang: a.toQuestion.learningObjectLanguage, full: 'true' }, | ||||
|         }; | ||||
| 
 | ||||
|         await getAnswerHandler(req as Request, res as Response); | ||||
|  | @ -68,11 +80,19 @@ describe('Questions controllers', () => { | |||
|         await expect(async () => getAnswerHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException); | ||||
|     }); | ||||
| 
 | ||||
|     it('Update question', async () => { | ||||
|         const newContent = 'updated question'; | ||||
|     it('Update answer', async () => { | ||||
|         const a = getAnswer02(); | ||||
|         const q = a.toQuestion; | ||||
| 
 | ||||
|         const newContent = 'updated answer'; | ||||
|         req = { | ||||
|             params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2' }, | ||||
|             query: { lang: Language.English }, | ||||
|             params: { | ||||
|                 hruid: q.learningObjectHruid, | ||||
|                 version: q.learningObjectVersion.toString(), | ||||
|                 seq: q.sequenceNumber!.toString(), | ||||
|                 seqAnswer: a.sequenceNumber!.toString(), | ||||
|             }, | ||||
|             query: { lang: q.learningObjectLanguage }, | ||||
|             body: { content: newContent }, | ||||
|         }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import { getAllQuestionsHandler, getQuestionHandler, updateQuestionHandler } fro | |||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { NotFoundException } from '../../src/exceptions/not-found-exception'; | ||||
| import { BadRequestException } from '../../src/exceptions/bad-request-exception'; | ||||
| import { getQuestion01 } from '../test_assets/questions/questions.testdata'; | ||||
| 
 | ||||
| describe('Questions controllers', () => { | ||||
|     let req: Partial<Request>; | ||||
|  | @ -24,9 +25,10 @@ describe('Questions controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get question list', async () => { | ||||
|         const q = getQuestion01(); | ||||
|         req = { | ||||
|             params: { hruid: 'id05', version: '1' }, | ||||
|             query: { lang: Language.English, full: 'true' }, | ||||
|             params: { hruid: q.learningObjectHruid, version: q.learningObjectVersion.toString() }, | ||||
|             query: { lang: q.learningObjectLanguage, full: 'true' }, | ||||
|         }; | ||||
| 
 | ||||
|         await getAllQuestionsHandler(req as Request, res as Response); | ||||
|  | @ -38,9 +40,10 @@ describe('Questions controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get question', async () => { | ||||
|         const q = getQuestion01(); | ||||
|         req = { | ||||
|             params: { hruid: 'id05', version: '1', seq: '1' }, | ||||
|             query: { lang: Language.English, full: 'true' }, | ||||
|             params: { hruid: q.learningObjectHruid, version: q.learningObjectVersion.toString(), seq: q.sequenceNumber!.toString() }, | ||||
|             query: { lang: q.learningObjectLanguage, full: 'true' }, | ||||
|         }; | ||||
| 
 | ||||
|         await getQuestionHandler(req as Request, res as Response); | ||||
|  | @ -51,8 +54,9 @@ describe('Questions controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get question with fallback sequence number and version', async () => { | ||||
|         const q = getQuestion01(); | ||||
|         req = { | ||||
|             params: { hruid: 'id05' }, | ||||
|             params: { hruid: q.learningObjectHruid }, | ||||
|             query: { lang: Language.English, full: 'true' }, | ||||
|         }; | ||||
| 
 | ||||
|  | @ -99,10 +103,11 @@ describe('Questions controllers', () => { | |||
|      */ | ||||
| 
 | ||||
|     it('Update question', async () => { | ||||
|         const q = getQuestion01(); | ||||
|         const newContent = 'updated question'; | ||||
|         req = { | ||||
|             params: { hruid: 'id05', version: '1', seq: '1' }, | ||||
|             query: { lang: Language.English }, | ||||
|             params: { hruid: q.learningObjectHruid, version: q.learningObjectVersion.toString(), seq: q.sequenceNumber!.toString() }, | ||||
|             query: { lang: q.learningObjectLanguage }, | ||||
|             body: { content: newContent }, | ||||
|         }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -15,13 +15,17 @@ import { | |||
|     deleteClassJoinRequestHandler, | ||||
|     getStudentRequestHandler, | ||||
| } from '../../src/controllers/students.js'; | ||||
| import { TEST_STUDENTS } from '../test_assets/users/students.testdata.js'; | ||||
| import { getDireStraits, getNoordkaap, getTheDoors, TEST_STUDENTS } from '../test_assets/users/students.testdata.js'; | ||||
| import { NotFoundException } from '../../src/exceptions/not-found-exception.js'; | ||||
| import { BadRequestException } from '../../src/exceptions/bad-request-exception.js'; | ||||
| import { ConflictException } from '../../src/exceptions/conflict-exception.js'; | ||||
| import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; | ||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||
| import { getClass02 } from '../test_assets/classes/classes.testdata'; | ||||
| import { getClass02 } from '../test_assets/classes/classes.testdata.js'; | ||||
| import { getClassJoinRequest02 } from '../test_assets/classes/class-join-requests.testdata.js'; | ||||
| import { getTestGroup01 } from '../test_assets/assignments/groups.testdata.js'; | ||||
| import { getSubmission01 } from '../test_assets/assignments/submission.testdata.js'; | ||||
| import { getQuestion01 } from '../test_assets/questions/questions.testdata.js'; | ||||
| 
 | ||||
| describe('Student controllers', () => { | ||||
|     let req: Partial<Request>; | ||||
|  | @ -41,7 +45,8 @@ describe('Student controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get student', async () => { | ||||
|         req = { params: { username: 'DireStraits' } }; | ||||
|         const student = getDireStraits(); | ||||
|         req = { params: { username: student.username } }; | ||||
| 
 | ||||
|         await getStudentHandler(req as Request, res as Response); | ||||
| 
 | ||||
|  | @ -83,11 +88,12 @@ describe('Student controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Create duplicate student', async () => { | ||||
|         const student = getDireStraits(); | ||||
|         req = { | ||||
|             body: { | ||||
|                 username: 'DireStraits', | ||||
|                 firstName: 'dupe', | ||||
|                 lastName: 'dupe', | ||||
|                 username: student.username, | ||||
|                 firstName: student.firstName, | ||||
|                 lastName: student.lastName, | ||||
|             }, | ||||
|         }; | ||||
| 
 | ||||
|  | @ -111,14 +117,17 @@ describe('Student controllers', () => { | |||
| 
 | ||||
|         // Check is DireStraits is part of the student list
 | ||||
|         const studentUsernames = result.students.map((s: StudentDTO) => s.username); | ||||
|         expect(studentUsernames).toContain('DireStraits'); | ||||
| 
 | ||||
|         expect(studentUsernames).toContain(TEST_STUDENTS[0].username); | ||||
| 
 | ||||
|         // Check length, +1 because of create
 | ||||
|         expect(result.students).toHaveLength(TEST_STUDENTS.length); | ||||
|     }); | ||||
| 
 | ||||
|     it('Student classes', async () => { | ||||
|         req = { params: { username: 'DireStraits' }, query: {} }; | ||||
|         const class_ = getClass02(); | ||||
|         const member = class_.students[0]; | ||||
|         req = { params: { username: member.username }, query: {} }; | ||||
| 
 | ||||
|         await getStudentClassesHandler(req as Request, res as Response); | ||||
| 
 | ||||
|  | @ -129,7 +138,9 @@ describe('Student controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Student groups', async () => { | ||||
|         req = { params: { username: 'DireStraits' }, query: {} }; | ||||
|         const group = getTestGroup01(); | ||||
|         const member = group.members[0]; | ||||
|         req = { params: { username: member.username }, query: {} }; | ||||
| 
 | ||||
|         await getStudentGroupsHandler(req as Request, res as Response); | ||||
| 
 | ||||
|  | @ -140,7 +151,8 @@ describe('Student controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Student submissions', async () => { | ||||
|         req = { params: { username: 'DireStraits' }, query: { full: 'true' } }; | ||||
|         const submission = getSubmission01(); | ||||
|         req = { params: { username: submission.submitter.username }, query: { full: 'true' } }; | ||||
| 
 | ||||
|         await getStudentSubmissionsHandler(req as Request, res as Response); | ||||
| 
 | ||||
|  | @ -151,7 +163,8 @@ describe('Student controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Student questions', async () => { | ||||
|         req = { params: { username: 'DireStraits' }, query: { full: 'true' } }; | ||||
|         const question = getQuestion01(); | ||||
|         req = { params: { username: question.author.username }, query: { full: 'true' } }; | ||||
| 
 | ||||
|         await getStudentQuestionsHandler(req as Request, res as Response); | ||||
| 
 | ||||
|  | @ -168,8 +181,9 @@ describe('Student controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get join requests by student', async () => { | ||||
|         const jr = getClassJoinRequest02(); | ||||
|         req = { | ||||
|             params: { username: 'PinkFloyd' }, | ||||
|             params: { username: jr.requester.username }, | ||||
|         }; | ||||
| 
 | ||||
|         await getStudentRequestsHandler(req as Request, res as Response); | ||||
|  | @ -186,8 +200,9 @@ describe('Student controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get join request by student and class', async () => { | ||||
|         const jr = getClassJoinRequest02(); | ||||
|         req = { | ||||
|             params: { username: 'PinkFloyd', classId: getClass02().classId }, | ||||
|             params: { username: jr.requester.username, classId: jr.class.classId! }, | ||||
|         }; | ||||
| 
 | ||||
|         await getStudentRequestHandler(req as Request, res as Response); | ||||
|  | @ -200,9 +215,11 @@ describe('Student controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Create and delete join request', async () => { | ||||
|         const student = getTheDoors(); | ||||
|         const class_ = getClass02(); | ||||
|         req = { | ||||
|             params: { username: 'TheDoors' }, | ||||
|             body: { classId: getClass02().classId }, | ||||
|             params: { username: student.username }, | ||||
|             body: { classId: class_.classId! }, | ||||
|         }; | ||||
| 
 | ||||
|         await createStudentRequestHandler(req as Request, res as Response); | ||||
|  | @ -210,7 +227,7 @@ describe('Student controllers', () => { | |||
|         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); | ||||
| 
 | ||||
|         req = { | ||||
|             params: { username: 'TheDoors', classId: getClass02().classId }, | ||||
|             params: { username: student.username, classId: class_.classId! }, | ||||
|         }; | ||||
| 
 | ||||
|         await deleteClassJoinRequestHandler(req as Request, res as Response); | ||||
|  | @ -221,18 +238,21 @@ describe('Student controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Create join request student already in class error', async () => { | ||||
|         const student = getNoordkaap(); | ||||
|         const class_ = getClass02(); | ||||
|         req = { | ||||
|             params: { username: 'Noordkaap' }, | ||||
|             body: { classId: getClass02().classId }, | ||||
|             params: { username: student.username }, | ||||
|             body: { classId: class_.classId! }, | ||||
|         }; | ||||
| 
 | ||||
|         await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException); | ||||
|     }); | ||||
| 
 | ||||
|     it('Create join request duplicate', async () => { | ||||
|         const jr = getClassJoinRequest02(); | ||||
|         req = { | ||||
|             params: { username: 'Tool' }, | ||||
|             body: { classId: getClass02().classId }, | ||||
|             params: { username: jr.requester.username }, | ||||
|             body: { classId: jr.class.classId! }, | ||||
|         }; | ||||
| 
 | ||||
|         await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException); | ||||
|  |  | |||
|  | @ -12,7 +12,9 @@ import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invit | |||
| import { getClassHandler } from '../../src/controllers/classes'; | ||||
| import { BadRequestException } from '../../src/exceptions/bad-request-exception'; | ||||
| import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; | ||||
| import { getClass02 } from '../test_assets/classes/classes.testdata'; | ||||
| import { getTeacherInvitation01 } from '../test_assets/classes/teacher-invitations.testdata.js'; | ||||
| import { getLimpBizkit, getTestleerkracht1 } from '../test_assets/users/teachers.testdata.js'; | ||||
| import { getClass02 } from '../test_assets/classes/classes.testdata.js'; | ||||
| 
 | ||||
| describe('Teacher controllers', () => { | ||||
|     let req: Partial<Request>; | ||||
|  | @ -32,7 +34,8 @@ describe('Teacher controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get teacher invitations by', async () => { | ||||
|         req = { params: { username: 'LimpBizkit' }, query: { sent: 'true' } }; | ||||
|         const ti = getTeacherInvitation01(); | ||||
|         req = { params: { username: ti.sender.username }, query: { sent: 'true' } }; | ||||
| 
 | ||||
|         await getAllInvitationsHandler(req as Request, res as Response); | ||||
| 
 | ||||
|  | @ -44,7 +47,8 @@ describe('Teacher controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get teacher invitations for', async () => { | ||||
|         req = { params: { username: 'FooFighters' }, query: { by: 'false' } }; | ||||
|         const ti = getTeacherInvitation01(); | ||||
|         req = { params: { username: ti.receiver.username }, query: { by: 'false' } }; | ||||
| 
 | ||||
|         await getAllInvitationsHandler(req as Request, res as Response); | ||||
| 
 | ||||
|  | @ -55,10 +59,13 @@ describe('Teacher controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Create and delete invitation', async () => { | ||||
|         const sender = getLimpBizkit(); | ||||
|         const receiver = getTestleerkracht1(); | ||||
|         const class_ = getClass02(); | ||||
|         const body = { | ||||
|             sender: 'LimpBizkit', | ||||
|             receiver: 'testleerkracht1', | ||||
|             class: getClass02().classId, | ||||
|             sender: sender.username, | ||||
|             receiver: receiver.username, | ||||
|             class: class_.classId, | ||||
|         } as TeacherInvitationData; | ||||
|         req = { body }; | ||||
| 
 | ||||
|  | @ -66,9 +73,9 @@ describe('Teacher controllers', () => { | |||
| 
 | ||||
|         req = { | ||||
|             params: { | ||||
|                 sender: 'LimpBizkit', | ||||
|                 receiver: 'testleerkracht1', | ||||
|                 classId: getClass02().classId, | ||||
|                 sender: sender.username, | ||||
|                 receiver: receiver.username, | ||||
|                 classId: class_.classId!, | ||||
|             }, | ||||
|             body: { accepted: 'false' }, | ||||
|         }; | ||||
|  | @ -77,11 +84,12 @@ describe('Teacher controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get invitation', async () => { | ||||
|         const ti = getTeacherInvitation01(); | ||||
|         req = { | ||||
|             params: { | ||||
|                 sender: 'LimpBizkit', | ||||
|                 receiver: 'FooFighters', | ||||
|                 classId: getClass02().classId, | ||||
|                 sender: ti.sender.username, | ||||
|                 receiver: ti.receiver.username, | ||||
|                 classId: ti.class.classId!, | ||||
|             }, | ||||
|         }; | ||||
|         await getInvitationHandler(req as Request, res as Response); | ||||
|  | @ -98,10 +106,11 @@ describe('Teacher controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Accept invitation', async () => { | ||||
|         const ti = getTeacherInvitation01(); | ||||
|         const body = { | ||||
|             sender: 'LimpBizkit', | ||||
|             receiver: 'FooFighters', | ||||
|             class: getClass02().classId, | ||||
|             sender: ti.sender.username, | ||||
|             receiver: ti.receiver.username, | ||||
|             class: ti.class.classId, | ||||
|         } as TeacherInvitationData; | ||||
|         req = { body }; | ||||
| 
 | ||||
|  | @ -112,13 +121,13 @@ describe('Teacher controllers', () => { | |||
| 
 | ||||
|         req = { | ||||
|             params: { | ||||
|                 id: getClass02().classId, | ||||
|                 id: ti.class.classId!, | ||||
|             }, | ||||
|         }; | ||||
| 
 | ||||
|         await getClassHandler(req as Request, res as Response); | ||||
| 
 | ||||
|         const result = jsonMock.mock.lastCall?.[0]; | ||||
|         expect(result.class.teachers).toContain('FooFighters'); | ||||
|         expect(result.class.teachers).toContain(ti.receiver.username); | ||||
|     }); | ||||
| }); | ||||
|  |  | |||
|  | @ -16,7 +16,9 @@ import { BadRequestException } from '../../src/exceptions/bad-request-exception. | |||
| import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; | ||||
| import { getStudentRequestsHandler } from '../../src/controllers/students.js'; | ||||
| import { getClassHandler } from '../../src/controllers/classes'; | ||||
| import { getClass02 } from '../test_assets/classes/classes.testdata'; | ||||
| import { getFooFighters, getTestleerkracht1 } from '../test_assets/users/teachers.testdata.js'; | ||||
| import { getClass02 } from '../test_assets/classes/classes.testdata.js'; | ||||
| import { getClassJoinRequest01 } from '../test_assets/classes/class-join-requests.testdata.js'; | ||||
| 
 | ||||
| describe('Teacher controllers', () => { | ||||
|     let req: Partial<Request>; | ||||
|  | @ -36,7 +38,8 @@ describe('Teacher controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Get teacher', async () => { | ||||
|         req = { params: { username: 'FooFighters' } }; | ||||
|         const teacher = getFooFighters(); | ||||
|         req = { params: { username: teacher.username } }; | ||||
| 
 | ||||
|         await getTeacherHandler(req as Request, res as Response); | ||||
| 
 | ||||
|  | @ -77,12 +80,13 @@ describe('Teacher controllers', () => { | |||
|         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ teacher: expect.objectContaining(teacher) })); | ||||
|     }); | ||||
| 
 | ||||
|     it('Create duplicate student', async () => { | ||||
|     it('Create duplicate teacher', async () => { | ||||
|         const teacher = getFooFighters(); | ||||
|         req = { | ||||
|             body: { | ||||
|                 username: 'FooFighters', | ||||
|                 firstName: 'Dave', | ||||
|                 lastName: 'Grohl', | ||||
|                 username: teacher.username, | ||||
|                 firstName: teacher.firstName, | ||||
|                 lastName: teacher.lastName, | ||||
|             }, | ||||
|         }; | ||||
| 
 | ||||
|  | @ -104,20 +108,22 @@ describe('Teacher controllers', () => { | |||
| 
 | ||||
|         const result = jsonMock.mock.lastCall?.[0]; | ||||
| 
 | ||||
|         expect(result.teachers).toContain('testleerkracht1'); | ||||
|         const teacher = getTestleerkracht1(); | ||||
|         expect(result.teachers).toContain(teacher.username); | ||||
| 
 | ||||
|         expect(result.teachers).toHaveLength(5); | ||||
|         expect(result.teachers.length).toBeGreaterThan(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('Deleting non-existent student', async () => { | ||||
|     it('Deleting non-existent teacher', async () => { | ||||
|         req = { params: { username: 'doesnotexist' } }; | ||||
| 
 | ||||
|         await expect(async () => deleteTeacherHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); | ||||
|     }); | ||||
| 
 | ||||
|     it('Get teacher classes', async () => { | ||||
|         const class_ = getClass02(); | ||||
|         req = { | ||||
|             params: { username: 'testleerkracht1' }, | ||||
|             params: { username: class_.teachers[0].username }, | ||||
|             query: { full: 'true' }, | ||||
|         }; | ||||
| 
 | ||||
|  | @ -130,9 +136,10 @@ describe('Teacher controllers', () => { | |||
|         expect(result.classes.length).toBeGreaterThan(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('Get teacher students', async () => { | ||||
|     it('Get teacher teachers', async () => { | ||||
|         const teacher = getTestleerkracht1(); | ||||
|         req = { | ||||
|             params: { username: 'testleerkracht1' }, | ||||
|             params: { username: teacher.username }, | ||||
|             query: { full: 'true' }, | ||||
|         }; | ||||
| 
 | ||||
|  | @ -145,30 +152,10 @@ describe('Teacher controllers', () => { | |||
|         expect(result.students.length).toBeGreaterThan(0); | ||||
|     }); | ||||
| 
 | ||||
|     /* | ||||
| 
 | ||||
|     It('Get teacher questions', async () => { | ||||
|         req = { | ||||
|             params: { username: 'FooFighters' }, | ||||
|             query: { full: 'true' }, | ||||
|         }; | ||||
| 
 | ||||
|         await getTeacherQuestionHandler(req as Request, res as Response); | ||||
| 
 | ||||
|         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() })); | ||||
| 
 | ||||
|         const result = jsonMock.mock.lastCall?.[0]; | ||||
|         // console.log('[TEACHER QUESTIONS]', result.questions);
 | ||||
|         expect(result.questions.length).toBeGreaterThan(0); | ||||
| 
 | ||||
|         // TODO fix
 | ||||
|     }); | ||||
| 
 | ||||
|      */ | ||||
| 
 | ||||
|     it('Get join requests by class', async () => { | ||||
|         const jr = getClassJoinRequest01(); | ||||
|         req = { | ||||
|             params: { classId: getClass02().classId }, | ||||
|             params: { classId: jr.class.classId! }, | ||||
|         }; | ||||
| 
 | ||||
|         await getStudentJoinRequestHandler(req as Request, res as Response); | ||||
|  | @ -181,8 +168,9 @@ describe('Teacher controllers', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('Update join request status', async () => { | ||||
|         const jr = getClassJoinRequest01(); | ||||
|         req = { | ||||
|             params: { classId: getClass02().classId, studentUsername: 'PinkFloyd' }, | ||||
|             params: { classId: jr.class.classId!, studentUsername: jr.requester.username }, | ||||
|             body: { accepted: 'true' }, | ||||
|         }; | ||||
| 
 | ||||
|  | @ -191,7 +179,7 @@ describe('Teacher controllers', () => { | |||
|         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); | ||||
| 
 | ||||
|         req = { | ||||
|             params: { username: 'PinkFloyd' }, | ||||
|             params: { username: jr.requester.username }, | ||||
|         }; | ||||
| 
 | ||||
|         await getStudentRequestsHandler(req as Request, res as Response); | ||||
|  | @ -200,11 +188,11 @@ describe('Teacher controllers', () => { | |||
|         expect(status).toBeTruthy(); | ||||
| 
 | ||||
|         req = { | ||||
|             params: { id: getClass02().classId }, | ||||
|             params: { id: jr.class.classId! }, | ||||
|         }; | ||||
| 
 | ||||
|         await getClassHandler(req as Request, res as Response); | ||||
|         const students: string[] = jsonMock.mock.lastCall?.[0].class.students; | ||||
|         expect(students).contains('PinkFloyd'); | ||||
|         expect(students).contains(jr.requester.username); | ||||
|     }); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,49 +1,60 @@ | |||
| import { beforeAll, describe, expect, it } from 'vitest'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository'; | ||||
| import { getAssignmentRepository, getClassRepository } from '../../../src/data/repositories'; | ||||
| import { ClassRepository } from '../../../src/data/classes/class-repository'; | ||||
| import { getAssignmentRepository } from '../../../src/data/repositories'; | ||||
| import { getClass02 } from '../../test_assets/classes/classes.testdata'; | ||||
| import { getAssignment02, getAssignment03 } from '../../test_assets/assignments/assignments.testdata'; | ||||
| import { getTestleerkracht1 } from '../../test_assets/users/teachers.testdata'; | ||||
| 
 | ||||
| describe('AssignmentRepository', () => { | ||||
|     let assignmentRepository: AssignmentRepository; | ||||
|     let classRepository: ClassRepository; | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         await setupTestApp(); | ||||
|         assignmentRepository = getAssignmentRepository(); | ||||
|         classRepository = getClassRepository(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return the requested assignment', async () => { | ||||
|         const class_ = await classRepository.findById(getClass02().classId); | ||||
|         const assignment = await assignmentRepository.findByClassAndId(class_!, 21001); | ||||
|         const class_ = getClass02(); | ||||
|         const usedAssignment = getAssignment02(); | ||||
|         const assignment = await assignmentRepository.findByClassAndId(class_, 21001); | ||||
| 
 | ||||
|         expect(assignment).toBeTruthy(); | ||||
|         expect(assignment!.title).toBe('tool'); | ||||
|         expect(assignment!.description).toBe(usedAssignment.description); | ||||
|         expect(assignment!.id).toBe(usedAssignment.id); | ||||
|         expect(assignment!.learningPathHruid).toBe(usedAssignment.learningPathHruid); | ||||
|         expect(assignment!.within.classId).toBe(usedAssignment.within.classId); | ||||
|         expect(assignment!.title).toBe(usedAssignment.title); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return all assignments for a class', async () => { | ||||
|         const class_ = await classRepository.findById(getClass02().classId); | ||||
|         const assignments = await assignmentRepository.findAllAssignmentsInClass(class_!); | ||||
|         const class_ = getClass02(); | ||||
|         const usedAssignment = getAssignment02(); | ||||
|         const assignments = await assignmentRepository.findAllAssignmentsInClass(class_); | ||||
| 
 | ||||
|         expect(assignments).toBeTruthy(); | ||||
|         expect(assignments).toHaveLength(1); | ||||
|         expect(assignments[0].title).toBe('tool'); | ||||
|         const assignment = assignments[0]; | ||||
|         expect(assignment.description).toBe(usedAssignment.description); | ||||
|         expect(assignment.id).toBe(usedAssignment.id); | ||||
|         expect(assignment.learningPathHruid).toBe(usedAssignment.learningPathHruid); | ||||
|         expect(assignment.within.classId).toBe(usedAssignment.within.classId); | ||||
|         expect(assignment.title).toBe(usedAssignment.title); | ||||
|     }); | ||||
| 
 | ||||
|     it('should find all by username of the responsible teacher', async () => { | ||||
|         const result = await assignmentRepository.findAllByResponsibleTeacher('testleerkracht1'); | ||||
|         const teacher = getTestleerkracht1(); | ||||
|         const result = await assignmentRepository.findAllByResponsibleTeacher(teacher.username); | ||||
|         const resultIds = result.map((it) => it.id).sort((a, b) => (a ?? 0) - (b ?? 0)); | ||||
| 
 | ||||
|         expect(resultIds).toEqual([21000, 21002, 21003, 21004]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not find removed assignment', async () => { | ||||
|         const class_ = await classRepository.findById('id01'); | ||||
|         await assignmentRepository.deleteByClassAndId(class_!, 3); | ||||
|         const deleted = getAssignment03(); | ||||
|         await assignmentRepository.deleteByClassAndId(deleted.within, deleted.id!); | ||||
| 
 | ||||
|         const assignment = await assignmentRepository.findByClassAndId(class_!, 3); | ||||
|         const assignment = await assignmentRepository.findByClassAndId(deleted.within, deleted.id!); | ||||
| 
 | ||||
|         expect(assignment).toBeNull(); | ||||
|     }); | ||||
|  |  | |||
|  | @ -1,50 +1,56 @@ | |||
| import { beforeAll, describe, expect, it } from 'vitest'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { GroupRepository } from '../../../src/data/assignments/group-repository'; | ||||
| import { getAssignmentRepository, getClassRepository, getGroupRepository } from '../../../src/data/repositories'; | ||||
| import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository'; | ||||
| import { ClassRepository } from '../../../src/data/classes/class-repository'; | ||||
| import { getClass01, getClass02 } from '../../test_assets/classes/classes.testdata'; | ||||
| import { getGroupRepository } from '../../../src/data/repositories'; | ||||
| import { getAssignment01, getAssignment02 } from '../../test_assets/assignments/assignments.testdata'; | ||||
| import { getTestGroup01, getTestGroup02, getTestGroup03 } from '../../test_assets/assignments/groups.testdata'; | ||||
| import { getDireStraits, getNoordkaap } from '../../test_assets/users/students.testdata'; | ||||
| 
 | ||||
| describe('GroupRepository', () => { | ||||
|     let groupRepository: GroupRepository; | ||||
|     let assignmentRepository: AssignmentRepository; | ||||
|     let classRepository: ClassRepository; | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         await setupTestApp(); | ||||
|         groupRepository = getGroupRepository(); | ||||
|         assignmentRepository = getAssignmentRepository(); | ||||
|         classRepository = getClassRepository(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return the requested group', async () => { | ||||
|         const id = getClass01().classId; | ||||
|         const class_ = await classRepository.findById(id); | ||||
|         const assignment = await assignmentRepository.findByClassAndId(class_!, 21000); | ||||
|         const assignment = getAssignment01(); | ||||
|         const usedGroup = getTestGroup01(); | ||||
|         const member1 = getNoordkaap(); | ||||
|         const member2 = getDireStraits(); | ||||
| 
 | ||||
|         const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001); | ||||
|         const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, usedGroup.groupNumber!); | ||||
| 
 | ||||
|         expect(group).toBeTruthy(); | ||||
|         expect(group?.groupNumber).toBe(usedGroup.groupNumber); | ||||
|         expect(group!.members[0].username).toBeOneOf([member1.username, member2.username]); | ||||
|         expect(group!.members[1].username).toBeOneOf([member1.username, member2.username]); | ||||
|         expect(group!.assignment.id).toBe(usedGroup.assignment.id); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return all groups for assignment', async () => { | ||||
|         const class_ = await classRepository.findById(getClass01().classId); | ||||
|         const assignment = await assignmentRepository.findByClassAndId(class_!, 21000); | ||||
|         const assignment = getAssignment01(); | ||||
|         const gr1 = getTestGroup01(); | ||||
|         const gr2 = getTestGroup02(); | ||||
|         const gr3 = getTestGroup03(); | ||||
| 
 | ||||
|         const groups = await groupRepository.findAllGroupsForAssignment(assignment!); | ||||
|         const groups = await groupRepository.findAllGroupsForAssignment(assignment); | ||||
| 
 | ||||
|         expect(groups).toBeTruthy(); | ||||
|         expect(groups).toHaveLength(3); | ||||
|         expect(groups[0].groupNumber).toBeOneOf([gr1.groupNumber, gr2.groupNumber, gr3.groupNumber]); | ||||
|         expect(groups[1].groupNumber).toBeOneOf([gr1.groupNumber, gr2.groupNumber, gr3.groupNumber]); | ||||
|         expect(groups[2].groupNumber).toBeOneOf([gr1.groupNumber, gr2.groupNumber, gr3.groupNumber]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not find removed group', async () => { | ||||
|         const class_ = await classRepository.findById(getClass02().classId); | ||||
|         const assignment = await assignmentRepository.findByClassAndId(class_!, 21001); | ||||
|         const assignment = getAssignment02(); | ||||
|         const deleted = getTestGroup01(); | ||||
| 
 | ||||
|         await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 21001); | ||||
|         await groupRepository.deleteByAssignmentAndGroupNumber(assignment, deleted.groupNumber!); | ||||
| 
 | ||||
|         const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1); | ||||
|         const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, deleted.groupNumber!); | ||||
| 
 | ||||
|         expect(group).toBeNull(); | ||||
|     }); | ||||
|  |  | |||
|  | @ -1,81 +1,75 @@ | |||
| import { beforeAll, describe, expect, it } from 'vitest'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { SubmissionRepository } from '../../../src/data/assignments/submission-repository'; | ||||
| import { | ||||
|     getAssignmentRepository, | ||||
|     getClassRepository, | ||||
|     getGroupRepository, | ||||
|     getStudentRepository, | ||||
|     getSubmissionRepository, | ||||
| } from '../../../src/data/repositories'; | ||||
| import { getSubmissionRepository } from '../../../src/data/repositories'; | ||||
| import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { StudentRepository } from '../../../src/data/users/student-repository'; | ||||
| import { GroupRepository } from '../../../src/data/assignments/group-repository'; | ||||
| import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository'; | ||||
| import { ClassRepository } from '../../../src/data/classes/class-repository'; | ||||
| import { Submission } from '../../../src/entities/assignments/submission.entity'; | ||||
| import { Class } from '../../../src/entities/classes/class.entity'; | ||||
| import { Assignment } from '../../../src/entities/assignments/assignment.entity'; | ||||
| import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata'; | ||||
| import { getClass01 } from '../../test_assets/classes/classes.testdata'; | ||||
| import { getSubmission01, getSubmission02, getSubmission07, getSubmission08 } from '../../test_assets/assignments/submission.testdata'; | ||||
| import { getAssignment01 } from '../../test_assets/assignments/assignments.testdata'; | ||||
| import { getTestGroup02 } from '../../test_assets/assignments/groups.testdata'; | ||||
| 
 | ||||
| describe('SubmissionRepository', () => { | ||||
|     let submissionRepository: SubmissionRepository; | ||||
|     let studentRepository: StudentRepository; | ||||
|     let groupRepository: GroupRepository; | ||||
|     let assignmentRepository: AssignmentRepository; | ||||
|     let classRepository: ClassRepository; | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         await setupTestApp(); | ||||
|         submissionRepository = getSubmissionRepository(); | ||||
|         studentRepository = getStudentRepository(); | ||||
|         groupRepository = getGroupRepository(); | ||||
|         assignmentRepository = getAssignmentRepository(); | ||||
|         classRepository = getClassRepository(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should find the requested submission', async () => { | ||||
|         const id = new LearningObjectIdentifier('id03', Language.English, 1); | ||||
|         const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1); | ||||
|         const usedSubmission = getSubmission01(); | ||||
|         const id = new LearningObjectIdentifier( | ||||
|             usedSubmission.learningObjectHruid, | ||||
|             usedSubmission.learningObjectLanguage, | ||||
|             usedSubmission.learningObjectVersion | ||||
|         ); | ||||
|         const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, usedSubmission.submissionNumber!); | ||||
| 
 | ||||
|         expect(submission).toBeTruthy(); | ||||
|         expect(submission?.content).toBe('sub1'); | ||||
|         expect(submission?.content).toBe(usedSubmission.content); | ||||
|         expect(submission?.submissionNumber).toBe(usedSubmission.submissionNumber); | ||||
|         expect(submission?.submitter.username).toBe(usedSubmission.submitter.username); | ||||
|     }); | ||||
| 
 | ||||
|     it('should find the most recent submission for a student', async () => { | ||||
|         const id = new LearningObjectIdentifier('id02', Language.English, 1); | ||||
|         const student = await studentRepository.findByUsername('Noordkaap'); | ||||
|         const submission = await submissionRepository.findMostRecentSubmissionForStudent(id, student!); | ||||
|         const usedSubmission = getSubmission02(); | ||||
|         const id = new LearningObjectIdentifier( | ||||
|             usedSubmission.learningObjectHruid, | ||||
|             usedSubmission.learningObjectLanguage, | ||||
|             usedSubmission.learningObjectVersion | ||||
|         ); | ||||
| 
 | ||||
|         const submission = await submissionRepository.findMostRecentSubmissionForStudent(id, usedSubmission.submitter); | ||||
| 
 | ||||
|         expect(submission).toBeTruthy(); | ||||
|         expect(submission?.submissionTime.getDate()).toBe(25); | ||||
|         expect(submission?.submissionTime).toStrictEqual(usedSubmission.submissionTime); | ||||
|     }); | ||||
| 
 | ||||
|     it('should find the most recent submission for a group', async () => { | ||||
|         const id = new LearningObjectIdentifier('id03', Language.English, 1); | ||||
|         const class_ = await classRepository.findById(getClass01().classId); | ||||
|         const assignment = await assignmentRepository.findByClassAndId(class_!, 21000); | ||||
|         const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001); | ||||
|         const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!); | ||||
|         const usedSubmission = getSubmission02(); | ||||
|         const id = new LearningObjectIdentifier( | ||||
|             usedSubmission.learningObjectHruid, | ||||
|             usedSubmission.learningObjectLanguage, | ||||
|             usedSubmission.learningObjectVersion | ||||
|         ); | ||||
| 
 | ||||
|         const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, usedSubmission.onBehalfOf); | ||||
| 
 | ||||
|         expect(submission).toBeTruthy(); | ||||
|         expect(submission?.submissionTime.getDate()).toBe(25); | ||||
|         expect(submission?.submissionTime).toStrictEqual(usedSubmission.submissionTime); | ||||
|     }); | ||||
| 
 | ||||
|     let clazz: Class | null; | ||||
|     let assignment: Assignment | null; | ||||
|     let loId: LearningObjectIdentifier; | ||||
|     it('should find all submissions for a certain learning object and assignment', async () => { | ||||
|         clazz = await classRepository.findById(getClass01().classId); | ||||
|         assignment = await assignmentRepository.findByClassAndId(clazz!, 21000); | ||||
|         loId = { | ||||
|             hruid: 'id02', | ||||
|             language: Language.English, | ||||
|             version: 1, | ||||
|         const usedSubmission = getSubmission08(); | ||||
|         const assignment = getAssignment01(); | ||||
| 
 | ||||
|         const loId = { | ||||
|             hruid: usedSubmission.learningObjectHruid, | ||||
|             language: usedSubmission.learningObjectLanguage, | ||||
|             version: usedSubmission.learningObjectVersion, | ||||
|         }; | ||||
|         const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!); | ||||
|         const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment); | ||||
|         sortSubmissions(result); | ||||
| 
 | ||||
|         expect(result).toHaveLength(3); | ||||
|  | @ -94,8 +88,15 @@ describe('SubmissionRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should find only the submissions for a certain learning object and assignment made for the given group', async () => { | ||||
|         const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21002); | ||||
|         const result = await submissionRepository.findAllSubmissionsForLearningObjectAndGroup(loId, group!); | ||||
|         const group = getTestGroup02(); | ||||
|         const usedSubmission = getSubmission08(); | ||||
|         const loId = { | ||||
|             hruid: usedSubmission.learningObjectHruid, | ||||
|             language: usedSubmission.learningObjectLanguage, | ||||
|             version: usedSubmission.learningObjectVersion, | ||||
|         }; | ||||
| 
 | ||||
|         const result = await submissionRepository.findAllSubmissionsForLearningObjectAndGroup(loId, group); | ||||
| 
 | ||||
|         expect(result).toHaveLength(1); | ||||
| 
 | ||||
|  | @ -108,10 +109,11 @@ describe('SubmissionRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should not find a deleted submission', async () => { | ||||
|         const usedSubmission = getSubmission07(); | ||||
|         const id = new LearningObjectIdentifier(testLearningObject01.hruid, testLearningObject01.language, testLearningObject01.version); | ||||
|         await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1); | ||||
|         await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, usedSubmission.submissionNumber!); | ||||
| 
 | ||||
|         const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1); | ||||
|         const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, usedSubmission.submissionNumber!); | ||||
| 
 | ||||
|         expect(submission).toBeNull(); | ||||
|     }); | ||||
|  |  | |||
|  | @ -1,45 +1,49 @@ | |||
| import { beforeAll, describe, expect, it } from 'vitest'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { ClassJoinRequestRepository } from '../../../src/data/classes/class-join-request-repository'; | ||||
| import { getClassJoinRequestRepository, getClassRepository, getStudentRepository } from '../../../src/data/repositories'; | ||||
| import { StudentRepository } from '../../../src/data/users/student-repository'; | ||||
| import { ClassRepository } from '../../../src/data/classes/class-repository'; | ||||
| import { getClassJoinRequestRepository } from '../../../src/data/repositories'; | ||||
| import { getPinkFloyd, getSmashingPumpkins } from '../../test_assets/users/students.testdata'; | ||||
| import { getClass02, getClass03 } from '../../test_assets/classes/classes.testdata'; | ||||
| import { getClassJoinRequest01, getClassJoinRequest02, getClassJoinRequest03 } from '../../test_assets/classes/class-join-requests.testdata'; | ||||
| 
 | ||||
| describe('ClassJoinRequestRepository', () => { | ||||
|     let classJoinRequestRepository: ClassJoinRequestRepository; | ||||
|     let studentRepository: StudentRepository; | ||||
|     let cassRepository: ClassRepository; | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         await setupTestApp(); | ||||
|         classJoinRequestRepository = getClassJoinRequestRepository(); | ||||
|         studentRepository = getStudentRepository(); | ||||
|         cassRepository = getClassRepository(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should list all requests from student to join classes', async () => { | ||||
|         const student = await studentRepository.findByUsername('PinkFloyd'); | ||||
|         const requests = await classJoinRequestRepository.findAllRequestsBy(student!); | ||||
|         const studentUsed = getPinkFloyd(); | ||||
|         const jr1 = getClassJoinRequest01(); | ||||
|         const jr2 = getClassJoinRequest03(); | ||||
|         const requests = await classJoinRequestRepository.findAllRequestsBy(studentUsed); | ||||
| 
 | ||||
|         expect(requests).toBeTruthy(); | ||||
|         expect(requests).toHaveLength(2); | ||||
|         expect(requests[0].class.classId!).toBeOneOf([jr1.class.classId!, jr2.class.classId!]); | ||||
|         expect(requests[1].class.classId!).toBeOneOf([jr1.class.classId!, jr2.class.classId!]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should list all requests to a single class', async () => { | ||||
|         const class_ = await cassRepository.findById(getClass02().classId); | ||||
|         const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_!); | ||||
|         const class_ = getClass02(); | ||||
|         const jr1 = getClassJoinRequest01(); | ||||
|         const jr2 = getClassJoinRequest02(); | ||||
|         const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_); | ||||
| 
 | ||||
|         expect(requests).toBeTruthy(); | ||||
|         expect(requests).toHaveLength(2); | ||||
|         expect(requests[0].class.classId).toBeOneOf([jr1.class.classId, jr2.class.classId]); | ||||
|         expect(requests[1].class.classId).toBeOneOf([jr1.class.classId, jr2.class.classId]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not find a removed request', async () => { | ||||
|         const student = await studentRepository.findByUsername('SmashingPumpkins'); | ||||
|         const class_ = await cassRepository.findById(getClass03().classId); | ||||
|         await classJoinRequestRepository.deleteBy(student!, class_!); | ||||
|         const studentUsed = getSmashingPumpkins(); | ||||
|         const class_ = getClass03(); | ||||
|         await classJoinRequestRepository.deleteBy(studentUsed, class_); | ||||
| 
 | ||||
|         const request = await classJoinRequestRepository.findAllRequestsBy(student!); | ||||
|         const request = await classJoinRequestRepository.findAllRequestsBy(studentUsed); | ||||
| 
 | ||||
|         expect(request).toHaveLength(0); | ||||
|     }); | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import { ClassRepository } from '../../../src/data/classes/class-repository'; | |||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { getClassRepository } from '../../../src/data/repositories'; | ||||
| import { getClass01, getClass04 } from '../../test_assets/classes/classes.testdata'; | ||||
| import { getClass01, getClass04 } from '../../test_assets/classes/classes.testdata'; | ||||
| 
 | ||||
| describe('ClassRepository', () => { | ||||
|     let classRepository: ClassRepository; | ||||
|  | @ -19,16 +20,18 @@ describe('ClassRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should return requested class', async () => { | ||||
|         const classVar = await classRepository.findById(getClass01().classId); | ||||
|         const expected = getClass01(); | ||||
|         const classVar = await classRepository.findById(expected.classId!); | ||||
| 
 | ||||
|         expect(classVar).toBeTruthy(); | ||||
|         expect(classVar?.displayName).toBe('class01'); | ||||
|         expect(classVar?.displayName).toBe(expected.displayName); | ||||
|     }); | ||||
| 
 | ||||
|     it('class should be gone after deletion', async () => { | ||||
|         await classRepository.deleteById(getClass04().classId); | ||||
|         const deleted = getClass04(); | ||||
|         await classRepository.deleteById(deleted.classId!); | ||||
| 
 | ||||
|         const classVar = await classRepository.findById(getClass04().classId); | ||||
|         const classVar = await classRepository.findById(deleted.classId!); | ||||
| 
 | ||||
|         expect(classVar).toBeNull(); | ||||
|     }); | ||||
|  |  | |||
|  | @ -1,54 +1,62 @@ | |||
| import { beforeAll, describe, expect, it } from 'vitest'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { getClassRepository, getTeacherInvitationRepository, getTeacherRepository } from '../../../src/data/repositories'; | ||||
| import { getTeacherInvitationRepository } from '../../../src/data/repositories'; | ||||
| import { TeacherInvitationRepository } from '../../../src/data/classes/teacher-invitation-repository'; | ||||
| import { TeacherRepository } from '../../../src/data/users/teacher-repository'; | ||||
| import { ClassRepository } from '../../../src/data/classes/class-repository'; | ||||
| import { getFooFighters, getLimpBizkit } from '../../test_assets/users/teachers.testdata'; | ||||
| import { getTeacherInvitation01, getTeacherInvitation02, getTeacherInvitation03 } from '../../test_assets/classes/teacher-invitations.testdata'; | ||||
| import { getClass01, getClass02 } from '../../test_assets/classes/classes.testdata'; | ||||
| 
 | ||||
| describe('ClassRepository', () => { | ||||
|     let teacherInvitationRepository: TeacherInvitationRepository; | ||||
|     let teacherRepository: TeacherRepository; | ||||
|     let classRepository: ClassRepository; | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         await setupTestApp(); | ||||
|         teacherInvitationRepository = getTeacherInvitationRepository(); | ||||
|         teacherRepository = getTeacherRepository(); | ||||
|         classRepository = getClassRepository(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return all invitations from a teacher', async () => { | ||||
|         const teacher = await teacherRepository.findByUsername('LimpBizkit'); | ||||
|         const invitations = await teacherInvitationRepository.findAllInvitationsBy(teacher!); | ||||
|         const teacher = getLimpBizkit(); | ||||
|         const ti1 = getTeacherInvitation01(); | ||||
|         const ti2 = getTeacherInvitation02(); | ||||
|         const invitations = await teacherInvitationRepository.findAllInvitationsBy(teacher); | ||||
| 
 | ||||
|         expect(invitations).toBeTruthy(); | ||||
|         expect(invitations).toHaveLength(2); | ||||
|         expect(invitations[0].class.classId).toBeOneOf([ti1.class.classId, ti2.class.classId]); | ||||
|         expect(invitations[1].class.classId).toBeOneOf([ti1.class.classId, ti2.class.classId]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return all invitations for a teacher', async () => { | ||||
|         const teacher = await teacherRepository.findByUsername('FooFighters'); | ||||
|         const invitations = await teacherInvitationRepository.findAllInvitationsFor(teacher!); | ||||
|         const teacher = getFooFighters(); | ||||
|         const ti1 = getTeacherInvitation01(); | ||||
|         const ti2 = getTeacherInvitation03(); | ||||
|         const invitations = await teacherInvitationRepository.findAllInvitationsFor(teacher); | ||||
| 
 | ||||
|         expect(invitations).toBeTruthy(); | ||||
|         expect(invitations).toHaveLength(2); | ||||
|         expect(invitations[0].class.classId).toBeOneOf([ti1.class.classId, ti2.class.classId]); | ||||
|         expect(invitations[1].class.classId).toBeOneOf([ti1.class.classId, ti2.class.classId]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return all invitations for a class', async () => { | ||||
|         const class_ = await classRepository.findById(getClass02().classId); | ||||
|         const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_!); | ||||
|         const class_ = getClass02(); | ||||
|         const ti1 = getTeacherInvitation01(); | ||||
|         const ti2 = getTeacherInvitation02(); | ||||
|         const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_); | ||||
| 
 | ||||
|         expect(invitations).toBeTruthy(); | ||||
|         expect(invitations).toHaveLength(2); | ||||
|         expect(invitations[0].class.classId).toBeOneOf([ti1.class.classId, ti2.class.classId]); | ||||
|         expect(invitations[1].class.classId).toBeOneOf([ti1.class.classId, ti2.class.classId]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not find a removed invitation', async () => { | ||||
|         const class_ = await classRepository.findById(getClass01().classId); | ||||
|         const sender = await teacherRepository.findByUsername('FooFighters'); | ||||
|         const receiver = await teacherRepository.findByUsername('LimpBizkit'); | ||||
|         await teacherInvitationRepository.deleteBy(class_!, sender!, receiver!); | ||||
|         const class_ = getClass01(); | ||||
|         const sender = getFooFighters(); | ||||
|         const receiver = getLimpBizkit(); | ||||
|         await teacherInvitationRepository.deleteBy(class_, sender, receiver); | ||||
| 
 | ||||
|         const invitation = await teacherInvitationRepository.findAllInvitationsBy(sender!); | ||||
|         const invitation = await teacherInvitationRepository.findAllInvitationsBy(sender); | ||||
| 
 | ||||
|         expect(invitation).toHaveLength(0); | ||||
|     }); | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest'; | |||
| import { setupTestApp } from '../../setup-tests.js'; | ||||
| import { getAttachmentRepository } from '../../../src/data/repositories.js'; | ||||
| import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js'; | ||||
| import { testLearningObject02 } from '../../test_assets/content/learning-objects.testdata'; | ||||
| import { getAttachment01 } from '../../test_assets/content/attachments.testdata.js'; | ||||
| 
 | ||||
| describe('AttachmentRepository', () => { | ||||
|     let attachmentRepository: AttachmentRepository; | ||||
|  | @ -13,10 +13,11 @@ describe('AttachmentRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should return the requested attachment', async () => { | ||||
|         const usedAttachment = getAttachment01(); | ||||
|         const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName( | ||||
|             testLearningObject02.hruid, | ||||
|             testLearningObject02.language, | ||||
|             'attachment01' | ||||
|             usedAttachment.learningObject.hruid, | ||||
|             usedAttachment.learningObject.language, | ||||
|             usedAttachment.name | ||||
|         ); | ||||
| 
 | ||||
|         expect(attachment).toBeTruthy(); | ||||
|  |  | |||
|  | @ -37,7 +37,8 @@ describe('LearningObjectRepository', () => { | |||
|     let newerExample: LearningObject; | ||||
| 
 | ||||
|     it('should allow a learning object with the same id except a different version to be added', async () => { | ||||
|         const testLearningObject01Newer = structuredClone(testLearningObject01); | ||||
|         // StructeredClone failed on teacher, this copies all fields to a json object
 | ||||
|         const testLearningObject01Newer = { ...testLearningObject01 }; | ||||
|         testLearningObject01Newer.version = 10; | ||||
|         testLearningObject01Newer.title += ' (nieuw)'; | ||||
|         testLearningObject01Newer.uuid = v4(); | ||||
|  |  | |||
|  | @ -1,48 +1,39 @@ | |||
| import { beforeAll, describe, expect, it } from 'vitest'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { AnswerRepository } from '../../../src/data/questions/answer-repository'; | ||||
| import { getAnswerRepository, getQuestionRepository, getTeacherRepository } from '../../../src/data/repositories'; | ||||
| import { QuestionRepository } from '../../../src/data/questions/question-repository'; | ||||
| import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { TeacherRepository } from '../../../src/data/users/teacher-repository'; | ||||
| import { getAnswerRepository } from '../../../src/data/repositories'; | ||||
| import { getQuestion01, getQuestion02 } from '../../test_assets/questions/questions.testdata'; | ||||
| import { getAnswer01, getAnswer02, getAnswer03 } from '../../test_assets/questions/answers.testdata'; | ||||
| import { getFooFighters } from '../../test_assets/users/teachers.testdata'; | ||||
| 
 | ||||
| describe('AnswerRepository', () => { | ||||
|     let answerRepository: AnswerRepository; | ||||
|     let questionRepository: QuestionRepository; | ||||
|     let teacherRepository: TeacherRepository; | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         await setupTestApp(); | ||||
|         answerRepository = getAnswerRepository(); | ||||
|         questionRepository = getQuestionRepository(); | ||||
|         teacherRepository = getTeacherRepository(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should find all answers to a question', async () => { | ||||
|         const id = new LearningObjectIdentifier('id05', Language.English, 1); | ||||
|         const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); | ||||
|         const question = getQuestion02(); | ||||
|         const a1 = getAnswer01(); | ||||
|         const a2 = getAnswer02(); | ||||
| 
 | ||||
|         const question = questions.find((it) => it.sequenceNumber === 2); | ||||
| 
 | ||||
|         const answers = await answerRepository.findAllAnswersToQuestion(question!); | ||||
|         const answers = await answerRepository.findAllAnswersToQuestion(question); | ||||
| 
 | ||||
|         expect(answers).toBeTruthy(); | ||||
|         expect(answers).toHaveLength(2); | ||||
|         expect(answers[0].content).toBeOneOf(['answer', 'answer2']); | ||||
|         expect(answers[1].content).toBeOneOf(['answer', 'answer2']); | ||||
|         expect(answers[0].content).toBeOneOf([a1.content, a2.content]); | ||||
|         expect(answers[1].content).toBeOneOf([a1.content, a2.content]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should create an answer to a question', async () => { | ||||
|         const teacher = await teacherRepository.findByUsername('FooFighters'); | ||||
|         const id = new LearningObjectIdentifier('id05', Language.English, 1); | ||||
|         const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); | ||||
| 
 | ||||
|         const question = questions[0]; | ||||
|         const teacher = getFooFighters(); | ||||
|         const question = getQuestion01(); | ||||
| 
 | ||||
|         await answerRepository.createAnswer({ | ||||
|             toQuestion: question, | ||||
|             author: teacher!, | ||||
|             author: teacher, | ||||
|             content: 'created answer', | ||||
|         }); | ||||
| 
 | ||||
|  | @ -54,12 +45,11 @@ describe('AnswerRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should not find a removed answer', async () => { | ||||
|         const id = new LearningObjectIdentifier('id04', Language.English, 1); | ||||
|         const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); | ||||
|         const deleted = getAnswer03(); | ||||
| 
 | ||||
|         await answerRepository.removeAnswerByQuestionAndSequenceNumber(questions[0], 1); | ||||
|         await answerRepository.removeAnswerByQuestionAndSequenceNumber(deleted.toQuestion, deleted.sequenceNumber!); | ||||
| 
 | ||||
|         const emptyList = await answerRepository.findAllAnswersToQuestion(questions[0]); | ||||
|         const emptyList = await answerRepository.findAllAnswersToQuestion(deleted.toQuestion); | ||||
| 
 | ||||
|         expect(emptyList).toHaveLength(0); | ||||
|     }); | ||||
|  |  | |||
|  | @ -1,70 +1,72 @@ | |||
| import { beforeAll, describe, expect, it } from 'vitest'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { QuestionRepository } from '../../../src/data/questions/question-repository'; | ||||
| import { | ||||
|     getAssignmentRepository, | ||||
|     getClassRepository, | ||||
|     getGroupRepository, | ||||
|     getQuestionRepository, | ||||
|     getStudentRepository, | ||||
| } from '../../../src/data/repositories'; | ||||
| import { StudentRepository } from '../../../src/data/users/student-repository'; | ||||
| import { getQuestionRepository } from '../../../src/data/repositories'; | ||||
| import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { Question } from '../../../src/entities/questions/question.entity'; | ||||
| import { Class } from '../../../src/entities/classes/class.entity'; | ||||
| import { Assignment } from '../../../src/entities/assignments/assignment.entity'; | ||||
| import { getClass01 } from '../../test_assets/classes/classes.testdata'; | ||||
| import { testLearningObject03, testLearningObject05 } from '../../test_assets/content/learning-objects.testdata'; | ||||
| import { getQuestion01, getQuestion02, getQuestion03, getQuestion05, getQuestion06 } from '../../test_assets/questions/questions.testdata'; | ||||
| import { getNoordkaap, getTool } from '../../test_assets/users/students.testdata'; | ||||
| import { getAssignment01 } from '../../test_assets/assignments/assignments.testdata'; | ||||
| import { getTestGroup01 } from '../../test_assets/assignments/groups.testdata'; | ||||
| 
 | ||||
| describe('QuestionRepository', () => { | ||||
|     let questionRepository: QuestionRepository; | ||||
|     let studentRepository: StudentRepository; | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         await setupTestApp(); | ||||
|         questionRepository = getQuestionRepository(); | ||||
|         studentRepository = getStudentRepository(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return all questions part of the given learning object', async () => { | ||||
|         const id = new LearningObjectIdentifier('id05', Language.English, 1); | ||||
|         const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); | ||||
|         const q1 = getQuestion01(); | ||||
|         const q2 = getQuestion02(); | ||||
|         const q3 = getQuestion05(); | ||||
|         const q4 = getQuestion06(); | ||||
|         const loid = { | ||||
|             hruid: q1.learningObjectHruid, | ||||
|             language: q1.learningObjectLanguage, | ||||
|             version: q1.learningObjectVersion, | ||||
|         } as LearningObjectIdentifier; | ||||
|         const questions = await questionRepository.findAllQuestionsAboutLearningObject(loid); | ||||
| 
 | ||||
|         expect(questions).toBeTruthy(); | ||||
|         expect(questions).toHaveLength(4); | ||||
|         expect(questions[0].sequenceNumber!).toBeOneOf([q1.sequenceNumber, q2.sequenceNumber, q3.sequenceNumber, q4.sequenceNumber]); | ||||
|         expect(questions[1].sequenceNumber!).toBeOneOf([q1.sequenceNumber, q2.sequenceNumber, q3.sequenceNumber, q4.sequenceNumber]); | ||||
|         expect(questions[2].sequenceNumber!).toBeOneOf([q1.sequenceNumber, q2.sequenceNumber, q3.sequenceNumber, q4.sequenceNumber]); | ||||
|         expect(questions[3].sequenceNumber!).toBeOneOf([q1.sequenceNumber, q2.sequenceNumber, q3.sequenceNumber, q4.sequenceNumber]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should create new question', async () => { | ||||
|         const id = new LearningObjectIdentifier('id03', Language.English, 1); | ||||
|         const student = await studentRepository.findByUsername('Noordkaap'); | ||||
| 
 | ||||
|         const clazz = await getClassRepository().findById(getClass01().classId); | ||||
|         const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000); | ||||
|         const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 21001); | ||||
|         const id = { | ||||
|             hruid: testLearningObject03.hruid, | ||||
|             language: testLearningObject03.language, | ||||
|             version: testLearningObject03.version, | ||||
|         } as LearningObjectIdentifier; | ||||
|         const student = getNoordkaap(); | ||||
|         const group = getTestGroup01(); | ||||
|         await questionRepository.createQuestion({ | ||||
|             loId: id, | ||||
|             inGroup: group!, | ||||
|             author: student!, | ||||
|             inGroup: group, | ||||
|             author: student, | ||||
|             content: 'question?', | ||||
|         }); | ||||
|         const question = await questionRepository.findAllQuestionsAboutLearningObject(id); | ||||
| 
 | ||||
|         expect(question).toBeTruthy(); | ||||
|         expect(question).toHaveLength(1); | ||||
|         expect(question[0].content).toBe('question?'); | ||||
|     }); | ||||
| 
 | ||||
|     let clazz: Class | null; | ||||
|     let assignment: Assignment | null; | ||||
|     let loId: LearningObjectIdentifier; | ||||
|     it('should find all questions for a certain learning object and assignment', async () => { | ||||
|         clazz = await getClassRepository().findById(getClass01().classId); | ||||
|         assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000); | ||||
|         loId = { | ||||
|             hruid: 'id05', | ||||
|             language: Language.English, | ||||
|             version: 1, | ||||
|         const assignment = getAssignment01(); | ||||
|         const loId = { | ||||
|             hruid: testLearningObject05.hruid, | ||||
|             language: testLearningObject05.language, | ||||
|             version: testLearningObject05.version, | ||||
|         }; | ||||
|         const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!); | ||||
|         const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment); | ||||
|         sortQuestions(result); | ||||
| 
 | ||||
|         expect(result).toHaveLength(3); | ||||
|  | @ -85,7 +87,14 @@ describe('QuestionRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it("should find only the questions for a certain learning object and assignment asked by the user's group", async () => { | ||||
|         const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, 'Tool'); | ||||
|         const loId = { | ||||
|             hruid: testLearningObject05.hruid, | ||||
|             language: testLearningObject05.language, | ||||
|             version: testLearningObject05.version, | ||||
|         }; | ||||
|         const assignment = getAssignment01(); | ||||
| 
 | ||||
|         const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment, getTool().username); | ||||
|         // (student Tool is in group #2)
 | ||||
| 
 | ||||
|         expect(result).toHaveLength(1); | ||||
|  | @ -100,12 +109,17 @@ describe('QuestionRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should not find removed question', async () => { | ||||
|         const id = new LearningObjectIdentifier('id04', Language.English, 1); | ||||
|         await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(id, 1); | ||||
|         const usedQuestion = getQuestion03(); | ||||
|         const id = { | ||||
|             hruid: usedQuestion.learningObjectHruid, | ||||
|             language: usedQuestion.learningObjectLanguage, | ||||
|             version: usedQuestion.learningObjectVersion, | ||||
|         } as LearningObjectIdentifier; | ||||
|         await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(id, usedQuestion.sequenceNumber!); | ||||
| 
 | ||||
|         const question = await questionRepository.findAllQuestionsAboutLearningObject(id); | ||||
|         const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); | ||||
| 
 | ||||
|         expect(question).toHaveLength(0); | ||||
|         expect(questions).toHaveLength(0); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { setupTestApp } from '../../setup-tests.js'; | |||
| import { describe, it, expect, beforeAll } from 'vitest'; | ||||
| import { StudentRepository } from '../../../src/data/users/student-repository.js'; | ||||
| import { getStudentRepository } from '../../../src/data/repositories.js'; | ||||
| import { getNoordkaap } from '../../test_assets/users/students.testdata.js'; | ||||
| 
 | ||||
| const username = 'teststudent'; | ||||
| const firstName = 'John'; | ||||
|  | @ -21,11 +22,12 @@ describe('StudentRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should return student from the datbase', async () => { | ||||
|         const student = await studentRepository.findByUsername('Noordkaap'); | ||||
|         const expectation = getNoordkaap(); | ||||
|         const student = await studentRepository.findByUsername(expectation.username); | ||||
| 
 | ||||
|         expect(student).toBeTruthy(); | ||||
|         expect(student?.firstName).toBe('Stijn'); | ||||
|         expect(student?.lastName).toBe('Meuris'); | ||||
|         expect(student?.firstName).toBe(expectation.firstName); | ||||
|         expect(student?.lastName).toBe(expectation.lastName); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return the queried student after he was added', async () => { | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; | |||
| import { TeacherRepository } from '../../../src/data/users/teacher-repository'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { getTeacherRepository } from '../../../src/data/repositories'; | ||||
| import { getFooFighters } from '../../test_assets/users/teachers.testdata'; | ||||
| 
 | ||||
| const username = 'testteacher'; | ||||
| const firstName = 'John'; | ||||
|  | @ -21,11 +22,12 @@ describe('TeacherRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should return teacher from the datbase', async () => { | ||||
|         const teacher = await teacherRepository.findByUsername('FooFighters'); | ||||
|         const expected = getFooFighters(); | ||||
|         const teacher = await teacherRepository.findByUsername(expected.username); | ||||
| 
 | ||||
|         expect(teacher).toBeTruthy(); | ||||
|         expect(teacher?.firstName).toBe('Dave'); | ||||
|         expect(teacher?.lastName).toBe('Grohl'); | ||||
|         expect(teacher?.firstName).toBe(expected.firstName); | ||||
|         expect(teacher?.lastName).toBe(expected.lastName); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return the queried teacher after he was added', async () => { | ||||
|  | @ -38,9 +40,9 @@ describe('TeacherRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should no longer return the queried teacher after he was removed again', async () => { | ||||
|         await teacherRepository.deleteByUsername('ZesdeMetaal'); | ||||
|         await teacherRepository.deleteByUsername(username); | ||||
| 
 | ||||
|         const retrievedTeacher = await teacherRepository.findByUsername('ZesdeMetaal'); | ||||
|         const retrievedTeacher = await teacherRepository.findByUsername(username); | ||||
|         expect(retrievedTeacher).toBeNull(); | ||||
|     }); | ||||
| }); | ||||
|  |  | |||
|  | @ -2,11 +2,11 @@ import { forkEntityManager, initORM } from '../src/orm.js'; | |||
| import dotenv from 'dotenv'; | ||||
| import { makeTestStudents } from './test_assets/users/students.testdata.js'; | ||||
| import { makeTestTeachers } from './test_assets/users/teachers.testdata.js'; | ||||
| import { makeTestLearningObjects } from './test_assets/content/learning-objects.testdata.js'; | ||||
| import { makeTestLearningObjects, testLearningObject01 } from './test_assets/content/learning-objects.testdata.js'; | ||||
| import { makeTestLearningPaths } from './test_assets/content/learning-paths.testdata.js'; | ||||
| import { makeTestClasses } from './test_assets/classes/classes.testdata.js'; | ||||
| import { makeTestAssignemnts } from './test_assets/assignments/assignments.testdata.js'; | ||||
| import { makeTestGroups } from './test_assets/assignments/groups.testdata.js'; | ||||
| import { getAssignment01, getAssignment02, makeTestAssignemnts } from './test_assets/assignments/assignments.testdata.js'; | ||||
| import { getTestGroup01, getTestGroup02, getTestGroup03, getTestGroup04, makeTestGroups } from './test_assets/assignments/groups.testdata.js'; | ||||
| import { makeTestTeacherInvitations } from './test_assets/classes/teacher-invitations.testdata.js'; | ||||
| import { makeTestClassJoinRequests } from './test_assets/classes/class-join-requests.testdata.js'; | ||||
| import { makeTestAttachments } from './test_assets/content/attachments.testdata.js'; | ||||
|  | @ -26,22 +26,26 @@ export async function setupTestApp(): Promise<void> { | |||
|     const teachers = makeTestTeachers(em); | ||||
|     const learningObjects = makeTestLearningObjects(em); | ||||
|     const learningPaths = makeTestLearningPaths(em); | ||||
|     const classes = makeTestClasses(em, students, teachers); | ||||
|     const assignments = makeTestAssignemnts(em, classes); | ||||
|     const groups = makeTestGroups(em, students, assignments); | ||||
|     const classes = makeTestClasses(em); | ||||
|     const assignments = makeTestAssignemnts(em); | ||||
|     const groups = makeTestGroups(em); | ||||
| 
 | ||||
|     assignments[0].groups = new Collection<Group>(groups.slice(0, 3)); | ||||
|     assignments[1].groups = new Collection<Group>(groups.slice(3, 4)); | ||||
|     const groups1 = [getTestGroup01(), getTestGroup02(), getTestGroup03()]; | ||||
|     const groups2 = [getTestGroup04()]; | ||||
|     const assignment1 = getAssignment01(); | ||||
|     const assignment2 = getAssignment02(); | ||||
|     assignment1.groups = new Collection<Group>(groups1); | ||||
|     assignment2.groups = new Collection<Group>(groups2); | ||||
| 
 | ||||
|     const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes); | ||||
|     const classJoinRequests = makeTestClassJoinRequests(em, students, classes); | ||||
|     const attachments = makeTestAttachments(em, learningObjects); | ||||
|     const teacherInvitations = makeTestTeacherInvitations(em); | ||||
|     const classJoinRequests = makeTestClassJoinRequests(em); | ||||
|     const attachments = makeTestAttachments(em); | ||||
| 
 | ||||
|     learningObjects[1].attachments = attachments; | ||||
|     testLearningObject01.attachments = attachments; | ||||
| 
 | ||||
|     const questions = makeTestQuestions(em, students, groups); | ||||
|     const answers = makeTestAnswers(em, teachers, questions); | ||||
|     const submissions = makeTestSubmissions(em, students, groups); | ||||
|     const questions = makeTestQuestions(em); | ||||
|     const answers = makeTestAnswers(em); | ||||
|     const submissions = makeTestSubmissions(em); | ||||
| 
 | ||||
|     await em.persistAndFlush([ | ||||
|         ...students, | ||||
|  |  | |||
|  | @ -1,11 +1,10 @@ | |||
| import { EntityManager } from '@mikro-orm/core'; | ||||
| import { Assignment } from '../../../src/entities/assignments/assignment.entity'; | ||||
| import { Class } from '../../../src/entities/classes/class.entity'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { testLearningPathWithConditions } from '../content/learning-paths.testdata'; | ||||
| import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata'; | ||||
| import { testLearningPath01, testLearningPath02, testLearningPathWithConditions } from '../content/learning-paths.testdata'; | ||||
| import { getClass01, getClass02, getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata'; | ||||
| 
 | ||||
| export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] { | ||||
| export function makeTestAssignemnts(em: EntityManager): Assignment[] { | ||||
|     const futureDate = new Date(); | ||||
|     futureDate.setDate(futureDate.getDate() + 7); | ||||
|     const pastDate = new Date(); | ||||
|  | @ -14,44 +13,44 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign | |||
|     today.setHours(23, 59); | ||||
|     assignment01 = em.create(Assignment, { | ||||
|         id: 21000, | ||||
|         within: classes[0], | ||||
|         within: getClass01(), | ||||
|         title: 'dire straits', | ||||
|         description: 'reading', | ||||
|         learningPathHruid: 'un_ai', | ||||
|         learningPathLanguage: Language.English, | ||||
|         learningPathHruid: testLearningPath02.hruid, | ||||
|         learningPathLanguage: testLearningPath02.language as Language, | ||||
|         deadline: today, | ||||
|         groups: [], | ||||
|     }); | ||||
| 
 | ||||
|     assignment02 = em.create(Assignment, { | ||||
|         id: 21001, | ||||
|         within: classes[1], | ||||
|         within: getClass02(), | ||||
|         title: 'tool', | ||||
|         description: 'reading', | ||||
|         learningPathHruid: 'id01', | ||||
|         learningPathLanguage: Language.English, | ||||
|         learningPathHruid: testLearningPath01.hruid, | ||||
|         learningPathLanguage: testLearningPath01.language as Language, | ||||
|         deadline: futureDate, | ||||
|         groups: [], | ||||
|     }); | ||||
| 
 | ||||
|     assignment03 = em.create(Assignment, { | ||||
|         id: 21002, | ||||
|         within: classes[0], | ||||
|         within: getClass01(), | ||||
|         title: 'delete', | ||||
|         description: 'will be deleted', | ||||
|         learningPathHruid: 'id02', | ||||
|         learningPathLanguage: Language.English, | ||||
|         learningPathHruid: testLearningPath02.hruid, | ||||
|         learningPathLanguage: testLearningPath02.language as Language, | ||||
|         deadline: pastDate, | ||||
|         groups: [], | ||||
|     }); | ||||
| 
 | ||||
|     assignment04 = em.create(Assignment, { | ||||
|         id: 21003, | ||||
|         within: classes[0], | ||||
|         within: getClass01(), | ||||
|         title: 'another assignment', | ||||
|         description: 'with a description', | ||||
|         learningPathHruid: 'id01', | ||||
|         learningPathLanguage: Language.English, | ||||
|         learningPathHruid: testLearningPath01.hruid, | ||||
|         learningPathLanguage: testLearningPath01.language as Language, | ||||
|         deadline: pastDate, | ||||
|         groups: [], | ||||
|     }); | ||||
|  |  | |||
|  | @ -1,19 +1,18 @@ | |||
| import { EntityManager } from '@mikro-orm/core'; | ||||
| import { Group } from '../../../src/entities/assignments/group.entity'; | ||||
| import { Assignment } from '../../../src/entities/assignments/assignment.entity'; | ||||
| import { Student } from '../../../src/entities/users/student.entity'; | ||||
| import { getConditionalPathAssignment } from './assignments.testdata'; | ||||
| import { getTestleerling1 } from '../users/students.testdata'; | ||||
| import { getAssignment01, getAssignment02, getAssignment04, getConditionalPathAssignment } from './assignments.testdata'; | ||||
| import { getDireStraits, getNoordkaap, getPinkFloyd, getSmashingPumpkins, getTestleerling1, getTheDoors, getTool } from '../users/students.testdata'; | ||||
| 
 | ||||
| export function makeTestGroups(em: EntityManager, students: Student[], assignments: Assignment[]): Group[] { | ||||
| export function makeTestGroups(em: EntityManager): Group[] { | ||||
|     /* | ||||
|      * Group #1 for Assignment #1 in class 'id01' | ||||
|      * => Assigned to do learning path 'id02' | ||||
|      */ | ||||
|     // Gets deleted
 | ||||
|     group01 = em.create(Group, { | ||||
|         assignment: assignments[0], | ||||
|         assignment: getAssignment01(), | ||||
|         groupNumber: 21001, | ||||
|         members: students.slice(0, 2), | ||||
|         members: [getNoordkaap(), getDireStraits()], | ||||
|     }); | ||||
| 
 | ||||
|     /* | ||||
|  | @ -21,9 +20,9 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen | |||
|      * => Assigned to do learning path 'id02' | ||||
|      */ | ||||
|     group02 = em.create(Group, { | ||||
|         assignment: assignments[0], | ||||
|         assignment: getAssignment01(), | ||||
|         groupNumber: 21002, | ||||
|         members: students.slice(2, 4), | ||||
|         members: [getTool(), getSmashingPumpkins()], | ||||
|     }); | ||||
| 
 | ||||
|     /* | ||||
|  | @ -31,9 +30,9 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen | |||
|      * => Assigned to do learning path 'id02' | ||||
|      */ | ||||
|     group03 = em.create(Group, { | ||||
|         assignment: assignments[0], | ||||
|         assignment: getAssignment01(), | ||||
|         groupNumber: 21003, | ||||
|         members: students.slice(4, 6), | ||||
|         members: [getPinkFloyd(), getTheDoors()], | ||||
|     }); | ||||
| 
 | ||||
|     /* | ||||
|  | @ -41,9 +40,9 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen | |||
|      * => Assigned to do learning path 'id01' | ||||
|      */ | ||||
|     group04 = em.create(Group, { | ||||
|         assignment: assignments[1], | ||||
|         assignment: getAssignment02(), | ||||
|         groupNumber: 21004, | ||||
|         members: students.slice(3, 4), | ||||
|         members: getSmashingPumpkins(), | ||||
|     }); | ||||
| 
 | ||||
|     /* | ||||
|  | @ -51,9 +50,9 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen | |||
|      * => Assigned to do learning path 'id01' | ||||
|      */ | ||||
|     group05 = em.create(Group, { | ||||
|         assignment: assignments[3], | ||||
|         assignment: getAssignment04(), | ||||
|         groupNumber: 21001, | ||||
|         members: students.slice(0, 2), | ||||
|         members: [getNoordkaap(), getDireStraits()], | ||||
|     }); | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -1,97 +1,139 @@ | |||
| import { EntityManager } from '@mikro-orm/core'; | ||||
| import { Submission } from '../../../src/entities/assignments/submission.entity'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { Student } from '../../../src/entities/users/student.entity'; | ||||
| import { Group } from '../../../src/entities/assignments/group.entity'; | ||||
| import { testLearningObject01, testLearningObject02, testLearningObject03 } from '../content/learning-objects.testdata'; | ||||
| import { getDireStraits, getNoordkaap, getSmashingPumpkins } from '../users/students.testdata'; | ||||
| import { getTestGroup01, getTestGroup02, getTestGroup04, getTestGroup05 } from './groups.testdata'; | ||||
| 
 | ||||
| export function makeTestSubmissions(em: EntityManager, students: Student[], groups: Group[]): Submission[] { | ||||
|     const submission01 = em.create(Submission, { | ||||
|         learningObjectHruid: 'id03', | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
| export function makeTestSubmissions(em: EntityManager): Submission[] { | ||||
|     submission01 = em.create(Submission, { | ||||
|         learningObjectHruid: testLearningObject03.hruid, | ||||
|         learningObjectLanguage: testLearningObject03.language, | ||||
|         learningObjectVersion: testLearningObject03.version, | ||||
|         submissionNumber: 1, | ||||
|         submitter: students[0], | ||||
|         submitter: getNoordkaap(), | ||||
|         submissionTime: new Date(2025, 2, 20), | ||||
|         onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         onBehalfOf: getTestGroup01(), // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         content: 'sub1', | ||||
|     }); | ||||
| 
 | ||||
|     const submission02 = em.create(Submission, { | ||||
|         learningObjectHruid: 'id03', | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|     submission02 = em.create(Submission, { | ||||
|         learningObjectHruid: testLearningObject03.hruid, | ||||
|         learningObjectLanguage: testLearningObject03.language, | ||||
|         learningObjectVersion: testLearningObject03.version, | ||||
|         submissionNumber: 2, | ||||
|         submitter: students[0], | ||||
|         submitter: getNoordkaap(), | ||||
|         submissionTime: new Date(2025, 2, 25), | ||||
|         onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         onBehalfOf: getTestGroup01(), // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         content: '', | ||||
|     }); | ||||
| 
 | ||||
|     const submission03 = em.create(Submission, { | ||||
|         learningObjectHruid: 'id02', | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|     submission03 = em.create(Submission, { | ||||
|         learningObjectHruid: testLearningObject02.hruid, | ||||
|         learningObjectLanguage: testLearningObject02.language, | ||||
|         learningObjectVersion: testLearningObject02.version, | ||||
|         submissionNumber: 1, | ||||
|         submitter: students[0], | ||||
|         submitter: getNoordkaap(), | ||||
|         submissionTime: new Date(2025, 2, 20), | ||||
|         onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         onBehalfOf: getTestGroup01(), // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         content: '', | ||||
|     }); | ||||
| 
 | ||||
|     const submission04 = em.create(Submission, { | ||||
|         learningObjectHruid: 'id02', | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|     submission04 = em.create(Submission, { | ||||
|         learningObjectHruid: testLearningObject02.hruid, | ||||
|         learningObjectLanguage: testLearningObject02.language, | ||||
|         learningObjectVersion: testLearningObject02.version, | ||||
|         submissionNumber: 2, | ||||
|         submitter: students[0], | ||||
|         submitter: getNoordkaap(), | ||||
|         submissionTime: new Date(2025, 2, 25), | ||||
|         onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         onBehalfOf: getTestGroup01(), // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         content: '', | ||||
|     }); | ||||
| 
 | ||||
|     const submission05 = em.create(Submission, { | ||||
|         learningObjectHruid: 'id01', | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|     submission05 = em.create(Submission, { | ||||
|         learningObjectHruid: testLearningObject01.hruid, | ||||
|         learningObjectLanguage: testLearningObject01.language, | ||||
|         learningObjectVersion: testLearningObject01.version, | ||||
|         submissionNumber: 1, | ||||
|         submitter: students[1], | ||||
|         submitter: getDireStraits(), | ||||
|         submissionTime: new Date(2025, 2, 20), | ||||
|         onBehalfOf: groups[1], // Group #2 for Assignment #1 in class 'id01'
 | ||||
|         onBehalfOf: getTestGroup02(), // Group #2 for Assignment #1 in class 'id01'
 | ||||
|         content: '', | ||||
|     }); | ||||
| 
 | ||||
|     const submission06 = em.create(Submission, { | ||||
|         learningObjectHruid: 'id01', | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|     submission06 = em.create(Submission, { | ||||
|         learningObjectHruid: testLearningObject01.hruid, | ||||
|         learningObjectLanguage: testLearningObject01.language, | ||||
|         learningObjectVersion: testLearningObject01.version, | ||||
|         submissionNumber: 2, | ||||
|         submitter: students[1], | ||||
|         submitter: getDireStraits(), | ||||
|         submissionTime: new Date(2025, 2, 25), | ||||
|         onBehalfOf: groups[4], // Group #5 for Assignment #4 in class 'id01'
 | ||||
|         onBehalfOf: getTestGroup05(), // Group #5 for Assignment #4 in class 'id01'
 | ||||
|         content: '', | ||||
|     }); | ||||
| 
 | ||||
|     const submission07 = em.create(Submission, { | ||||
|         learningObjectHruid: 'id01', | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|     // Gets deleted
 | ||||
|     submission07 = em.create(Submission, { | ||||
|         learningObjectHruid: testLearningObject01.hruid, | ||||
|         learningObjectLanguage: testLearningObject01.language, | ||||
|         learningObjectVersion: testLearningObject01.version, | ||||
|         submissionNumber: 3, | ||||
|         submitter: students[3], | ||||
|         submitter: getSmashingPumpkins(), | ||||
|         submissionTime: new Date(2025, 3, 25), | ||||
|         onBehalfOf: groups[3], // Group #4 for Assignment #2 in class 'id02'
 | ||||
|         onBehalfOf: getTestGroup04(), // Group #4 for Assignment #2 in class 'id02'
 | ||||
|         content: '', | ||||
|     }); | ||||
| 
 | ||||
|     const submission08 = em.create(Submission, { | ||||
|         learningObjectHruid: 'id02', | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|     submission08 = em.create(Submission, { | ||||
|         learningObjectHruid: testLearningObject02.hruid, | ||||
|         learningObjectLanguage: testLearningObject02.language, | ||||
|         learningObjectVersion: testLearningObject02.version, | ||||
|         submissionNumber: 3, | ||||
|         submitter: students[1], | ||||
|         submitter: getDireStraits(), | ||||
|         submissionTime: new Date(2025, 4, 7), | ||||
|         onBehalfOf: groups[1], // Group #2 for Assignment #1 in class 'id01'
 | ||||
|         onBehalfOf: getTestGroup02(), // Group #2 for Assignment #1 in class 'id01'
 | ||||
|         content: '', | ||||
|     }); | ||||
| 
 | ||||
|     return [submission01, submission02, submission03, submission04, submission05, submission06, submission07, submission08]; | ||||
| } | ||||
| 
 | ||||
| let submission01: Submission; | ||||
| let submission02: Submission; | ||||
| let submission03: Submission; | ||||
| let submission04: Submission; | ||||
| let submission05: Submission; | ||||
| let submission06: Submission; | ||||
| let submission07: Submission; | ||||
| let submission08: Submission; | ||||
| 
 | ||||
| export function getSubmission01(): Submission { | ||||
|     return submission01; | ||||
| } | ||||
| 
 | ||||
| export function getSubmission02(): Submission { | ||||
|     return submission02; | ||||
| } | ||||
| 
 | ||||
| export function getSubmission03(): Submission { | ||||
|     return submission03; | ||||
| } | ||||
| 
 | ||||
| export function getSubmission04(): Submission { | ||||
|     return submission04; | ||||
| } | ||||
| 
 | ||||
| export function getSubmission05(): Submission { | ||||
|     return submission05; | ||||
| } | ||||
| 
 | ||||
| export function getSubmission06(): Submission { | ||||
|     return submission06; | ||||
| } | ||||
| 
 | ||||
| export function getSubmission07(): Submission { | ||||
|     return submission07; | ||||
| } | ||||
| 
 | ||||
| export function getSubmission08(): Submission { | ||||
|     return submission08; | ||||
| } | ||||
|  |  | |||
|  | @ -1,33 +1,54 @@ | |||
| import { EntityManager } from '@mikro-orm/core'; | ||||
| import { ClassJoinRequest } from '../../../src/entities/classes/class-join-request.entity'; | ||||
| import { Student } from '../../../src/entities/users/student.entity'; | ||||
| import { Class } from '../../../src/entities/classes/class.entity'; | ||||
| import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; | ||||
| import { getPinkFloyd, getSmashingPumpkins, getTool } from '../users/students.testdata'; | ||||
| import { getClass02, getClass03 } from './classes.testdata'; | ||||
| 
 | ||||
| export function makeTestClassJoinRequests(em: EntityManager, students: Student[], classes: Class[]): ClassJoinRequest[] { | ||||
|     const classJoinRequest01 = em.create(ClassJoinRequest, { | ||||
|         requester: students[4], | ||||
|         class: classes[1], | ||||
| export function makeTestClassJoinRequests(em: EntityManager): ClassJoinRequest[] { | ||||
|     classJoinRequest01 = em.create(ClassJoinRequest, { | ||||
|         requester: getPinkFloyd(), | ||||
|         class: getClass02(), | ||||
|         status: ClassStatus.Open, | ||||
|     }); | ||||
| 
 | ||||
|     const classJoinRequest02 = em.create(ClassJoinRequest, { | ||||
|         requester: students[2], | ||||
|         class: classes[1], | ||||
|     classJoinRequest02 = em.create(ClassJoinRequest, { | ||||
|         requester: getTool(), | ||||
|         class: getClass02(), | ||||
|         status: ClassStatus.Open, | ||||
|     }); | ||||
| 
 | ||||
|     const classJoinRequest03 = em.create(ClassJoinRequest, { | ||||
|         requester: students[4], | ||||
|         class: classes[2], | ||||
|     classJoinRequest03 = em.create(ClassJoinRequest, { | ||||
|         requester: getPinkFloyd(), | ||||
|         class: getClass03(), | ||||
|         status: ClassStatus.Open, | ||||
|     }); | ||||
| 
 | ||||
|     const classJoinRequest04 = em.create(ClassJoinRequest, { | ||||
|         requester: students[3], | ||||
|         class: classes[2], | ||||
|     classJoinRequest04 = em.create(ClassJoinRequest, { | ||||
|         requester: getSmashingPumpkins(), | ||||
|         class: getClass03(), | ||||
|         status: ClassStatus.Open, | ||||
|     }); | ||||
| 
 | ||||
|     return [classJoinRequest01, classJoinRequest02, classJoinRequest03, classJoinRequest04]; | ||||
| } | ||||
| 
 | ||||
| let classJoinRequest01: ClassJoinRequest; | ||||
| let classJoinRequest02: ClassJoinRequest; | ||||
| let classJoinRequest03: ClassJoinRequest; | ||||
| let classJoinRequest04: ClassJoinRequest; | ||||
| 
 | ||||
| export function getClassJoinRequest01(): ClassJoinRequest { | ||||
|     return classJoinRequest01; | ||||
| } | ||||
| 
 | ||||
| export function getClassJoinRequest02(): ClassJoinRequest { | ||||
|     return classJoinRequest02; | ||||
| } | ||||
| 
 | ||||
| export function getClassJoinRequest03(): ClassJoinRequest { | ||||
|     return classJoinRequest03; | ||||
| } | ||||
| 
 | ||||
| export function getClassJoinRequest04(): ClassJoinRequest { | ||||
|     return classJoinRequest04; | ||||
| } | ||||
|  |  | |||
|  | @ -2,12 +2,12 @@ import { EntityManager } from '@mikro-orm/core'; | |||
| import { Class } from '../../../src/entities/classes/class.entity'; | ||||
| import { Student } from '../../../src/entities/users/student.entity'; | ||||
| import { Teacher } from '../../../src/entities/users/teacher.entity'; | ||||
| import { getTestleerkracht1 } from '../users/teachers.testdata'; | ||||
| import { getTestleerling1 } from '../users/students.testdata'; | ||||
| import { getLimpBizkit, getStaind, getTestleerkracht1 } from '../users/teachers.testdata'; | ||||
| import { getDireStraits, getNoordkaap, getSmashingPumpkins, getTestleerling1, getTool } from '../users/students.testdata'; | ||||
| 
 | ||||
| export function makeTestClasses(em: EntityManager, students: Student[], teachers: Teacher[]): Class[] { | ||||
|     const studentsClass01 = students.slice(0, 8); | ||||
|     const teacherClass01: Teacher[] = teachers.slice(4, 5); | ||||
| export function makeTestClasses(em: EntityManager): Class[] { | ||||
|     const studentsClass01 = [getTestleerling1()]; | ||||
|     const teacherClass01: Teacher[] = [getTestleerkracht1()]; | ||||
| 
 | ||||
|     class01 = em.create(Class, { | ||||
|         classId: 'X2J9QT', // 8764b861-90a6-42e5-9732-c0d9eb2f55f9
 | ||||
|  | @ -16,8 +16,8 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers | |||
|         students: studentsClass01, | ||||
|     }); | ||||
| 
 | ||||
|     const studentsClass02: Student[] = students.slice(0, 2).concat(students.slice(3, 4)); | ||||
|     const teacherClass02: Teacher[] = teachers.slice(1, 2); | ||||
|     const studentsClass02: Student[] = [getNoordkaap(), getDireStraits(), getSmashingPumpkins()]; | ||||
|     const teacherClass02: Teacher[] = [getLimpBizkit()]; | ||||
| 
 | ||||
|     class02 = em.create(Class, { | ||||
|         classId: '7KLPMA', // 34d484a1-295f-4e9f-bfdc-3e7a23d86a89
 | ||||
|  | @ -26,8 +26,8 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers | |||
|         students: studentsClass02, | ||||
|     }); | ||||
| 
 | ||||
|     const studentsClass03: Student[] = students.slice(1, 4); | ||||
|     const teacherClass03: Teacher[] = teachers.slice(2, 3); | ||||
|     const studentsClass03: Student[] = [getDireStraits(), getTool(), getSmashingPumpkins()]; | ||||
|     const teacherClass03: Teacher[] = [getStaind()]; | ||||
| 
 | ||||
|     class03 = em.create(Class, { | ||||
|         classId: 'R0D3UZ', // 80dcc3e0-1811-4091-9361-42c0eee91cfa
 | ||||
|  | @ -36,9 +36,10 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers | |||
|         students: studentsClass03, | ||||
|     }); | ||||
| 
 | ||||
|     const studentsClass04: Student[] = students.slice(0, 2); | ||||
|     const teacherClass04: Teacher[] = teachers.slice(2, 3); | ||||
|     const studentsClass04: Student[] = [getNoordkaap(), getDireStraits()]; | ||||
|     const teacherClass04: Teacher[] = [getStaind()]; | ||||
| 
 | ||||
|     // Gets deleted in test
 | ||||
|     class04 = em.create(Class, { | ||||
|         classId: 'Q8N5YC', // 33d03536-83b8-4880-9982-9bbf2f908ddf
 | ||||
|         displayName: 'class04', | ||||
|  |  | |||
|  | @ -1,37 +1,59 @@ | |||
| import { EntityManager } from '@mikro-orm/core'; | ||||
| import { TeacherInvitation } from '../../../src/entities/classes/teacher-invitation.entity'; | ||||
| import { Teacher } from '../../../src/entities/users/teacher.entity'; | ||||
| import { Class } from '../../../src/entities/classes/class.entity'; | ||||
| import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; | ||||
| import { getFooFighters, getLimpBizkit, getStaind } from '../users/teachers.testdata'; | ||||
| import { getClass01, getClass02, getClass03 } from './classes.testdata'; | ||||
| 
 | ||||
| export function makeTestTeacherInvitations(em: EntityManager, teachers: Teacher[], classes: Class[]): TeacherInvitation[] { | ||||
|     const teacherInvitation01 = em.create(TeacherInvitation, { | ||||
|         sender: teachers[1], | ||||
|         receiver: teachers[0], | ||||
|         class: classes[1], | ||||
| export function makeTestTeacherInvitations(em: EntityManager): TeacherInvitation[] { | ||||
|     teacherInvitation01 = em.create(TeacherInvitation, { | ||||
|         sender: getLimpBizkit(), | ||||
|         receiver: getFooFighters(), | ||||
|         class: getClass02(), | ||||
|         status: ClassStatus.Open, | ||||
|     }); | ||||
| 
 | ||||
|     const teacherInvitation02 = em.create(TeacherInvitation, { | ||||
|         sender: teachers[1], | ||||
|         receiver: teachers[2], | ||||
|         class: classes[1], | ||||
|     teacherInvitation02 = em.create(TeacherInvitation, { | ||||
|         sender: getLimpBizkit(), | ||||
|         receiver: getStaind(), | ||||
|         class: getClass02(), | ||||
|         status: ClassStatus.Open, | ||||
|     }); | ||||
| 
 | ||||
|     const teacherInvitation03 = em.create(TeacherInvitation, { | ||||
|         sender: teachers[2], | ||||
|         receiver: teachers[0], | ||||
|         class: classes[2], | ||||
|     teacherInvitation03 = em.create(TeacherInvitation, { | ||||
|         sender: getStaind(), | ||||
|         receiver: getFooFighters(), | ||||
|         class: getClass03(), | ||||
|         status: ClassStatus.Open, | ||||
|     }); | ||||
| 
 | ||||
|     const teacherInvitation04 = em.create(TeacherInvitation, { | ||||
|         sender: teachers[0], | ||||
|         receiver: teachers[1], | ||||
|         class: classes[0], | ||||
|     // Gets deleted in test
 | ||||
|     teacherInvitation04 = em.create(TeacherInvitation, { | ||||
|         sender: getFooFighters(), | ||||
|         receiver: getLimpBizkit(), | ||||
|         class: getClass01(), | ||||
|         status: ClassStatus.Open, | ||||
|     }); | ||||
| 
 | ||||
|     return [teacherInvitation01, teacherInvitation02, teacherInvitation03, teacherInvitation04]; | ||||
| } | ||||
| 
 | ||||
| let teacherInvitation01: TeacherInvitation; | ||||
| let teacherInvitation02: TeacherInvitation; | ||||
| let teacherInvitation03: TeacherInvitation; | ||||
| let teacherInvitation04: TeacherInvitation; | ||||
| 
 | ||||
| export function getTeacherInvitation01(): TeacherInvitation { | ||||
|     return teacherInvitation01; | ||||
| } | ||||
| 
 | ||||
| export function getTeacherInvitation02(): TeacherInvitation { | ||||
|     return teacherInvitation02; | ||||
| } | ||||
| 
 | ||||
| export function getTeacherInvitation03(): TeacherInvitation { | ||||
|     return teacherInvitation03; | ||||
| } | ||||
| 
 | ||||
| export function getTeacherInvitation04(): TeacherInvitation { | ||||
|     return teacherInvitation04; | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,14 @@ | |||
| import { EntityManager } from '@mikro-orm/core'; | ||||
| import { Attachment } from '../../../src/entities/content/attachment.entity'; | ||||
| import { testLearningObject01 } from './learning-objects.testdata'; | ||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; | ||||
| 
 | ||||
| export function makeTestAttachments(em: EntityManager, learningObjects: LearningObject[]): Attachment[] { | ||||
|     const attachment01 = em.create(Attachment, { | ||||
|         learningObject: learningObjects[1], | ||||
| export function makeTestAttachments(em: EntityManager): Attachment[] { | ||||
|     // Prevent duplicate insertion
 | ||||
|     const lo = em.merge(LearningObject, testLearningObject01); | ||||
| 
 | ||||
|     attachment01 = em.create(Attachment, { | ||||
|         learningObject: lo, | ||||
|         name: 'attachment01', | ||||
|         mimeType: '', | ||||
|         content: Buffer.from(''), | ||||
|  | @ -12,3 +16,9 @@ export function makeTestAttachments(em: EntityManager, learningObjects: Learning | |||
| 
 | ||||
|     return [attachment01]; | ||||
| } | ||||
| 
 | ||||
| let attachment01: Attachment; | ||||
| 
 | ||||
| export function getAttachment01(): Attachment { | ||||
|     return attachment01; | ||||
| } | ||||
|  |  | |||
|  | @ -75,6 +75,7 @@ export const testLearningObject02: RequiredEntityData<LearningObject> = { | |||
|     description: 'second album', | ||||
|     contentType: DwengoContentType.TEXT_MARKDOWN, | ||||
|     keywords: [], | ||||
|     uuid: v4(), | ||||
|     teacherExclusive: false, | ||||
|     skosConcepts: [], | ||||
|     educationalGoals: [], | ||||
|  | @ -99,6 +100,7 @@ export const testLearningObject03: RequiredEntityData<LearningObject> = { | |||
|     description: 'third album', | ||||
|     contentType: DwengoContentType.TEXT_MARKDOWN, | ||||
|     keywords: [], | ||||
|     uuid: v4(), | ||||
|     teacherExclusive: false, | ||||
|     skosConcepts: [], | ||||
|     educationalGoals: [], | ||||
|  | @ -126,6 +128,7 @@ export const testLearningObject04: RequiredEntityData<LearningObject> = { | |||
|     description: 'fifth album', | ||||
|     contentType: DwengoContentType.TEXT_MARKDOWN, | ||||
|     keywords: [], | ||||
|     uuid: v4(), | ||||
|     teacherExclusive: false, | ||||
|     skosConcepts: [], | ||||
|     educationalGoals: [], | ||||
|  | @ -153,6 +156,7 @@ export const testLearningObject05: RequiredEntityData<LearningObject> = { | |||
|     description: 'sixth album', | ||||
|     contentType: DwengoContentType.TEXT_MARKDOWN, | ||||
|     keywords: [], | ||||
|     uuid: v4(), | ||||
|     teacherExclusive: false, | ||||
|     skosConcepts: [], | ||||
|     educationalGoals: [], | ||||
|  | @ -173,6 +177,7 @@ export const testLearningObjectMultipleChoice: RequiredEntityData<LearningObject | |||
|     title: 'Self-evaluation', | ||||
|     description: "Time to evaluate how well you understand what you've learned so far.", | ||||
|     keywords: ['test'], | ||||
|     uuid: v4(), | ||||
|     teacherExclusive: false, | ||||
|     skosConcepts: [], | ||||
|     educationalGoals: [], | ||||
|  | @ -199,6 +204,7 @@ export const testLearningObjectEssayQuestion: RequiredEntityData<LearningObject> | |||
|     title: 'Reflection', | ||||
|     description: 'Reflect on your learning progress.', | ||||
|     keywords: ['test'], | ||||
|     uuid: v4(), | ||||
|     teacherExclusive: false, | ||||
|     skosConcepts: [], | ||||
|     educationalGoals: [], | ||||
|  |  | |||
|  | @ -1,36 +1,35 @@ | |||
| import { EntityManager } from '@mikro-orm/core'; | ||||
| import { Answer } from '../../../src/entities/questions/answer.entity'; | ||||
| import { Teacher } from '../../../src/entities/users/teacher.entity'; | ||||
| import { Question } from '../../../src/entities/questions/question.entity'; | ||||
| import { getTestleerkracht1 } from '../users/teachers.testdata'; | ||||
| import { getQuestion07 } from './questions.testdata'; | ||||
| import { getFooFighters, getLimpBizkit, getTestleerkracht1 } from '../users/teachers.testdata'; | ||||
| import { getQuestion02, getQuestion04, getQuestion07 } from './questions.testdata'; | ||||
| 
 | ||||
| export function makeTestAnswers(em: EntityManager, teachers: Teacher[], questions: Question[]): Answer[] { | ||||
|     const answer01 = em.create(Answer, { | ||||
|         author: teachers[0], | ||||
|         toQuestion: questions[1], | ||||
| export function makeTestAnswers(em: EntityManager): Answer[] { | ||||
|     answer01 = em.create(Answer, { | ||||
|         author: getFooFighters(), | ||||
|         toQuestion: getQuestion02(), | ||||
|         sequenceNumber: 1, | ||||
|         timestamp: new Date(), | ||||
|         content: 'answer', | ||||
|     }); | ||||
| 
 | ||||
|     const answer02 = em.create(Answer, { | ||||
|         author: teachers[0], | ||||
|         toQuestion: questions[1], | ||||
|     answer02 = em.create(Answer, { | ||||
|         author: getFooFighters(), | ||||
|         toQuestion: getQuestion02(), | ||||
|         sequenceNumber: 2, | ||||
|         timestamp: new Date(), | ||||
|         content: 'answer2', | ||||
|     }); | ||||
| 
 | ||||
|     const answer03 = em.create(Answer, { | ||||
|         author: teachers[1], | ||||
|         toQuestion: questions[3], | ||||
|     // Gets deleted
 | ||||
|     answer03 = em.create(Answer, { | ||||
|         author: getLimpBizkit(), | ||||
|         toQuestion: getQuestion04(), | ||||
|         sequenceNumber: 1, | ||||
|         timestamp: new Date(), | ||||
|         content: 'answer3', | ||||
|     }); | ||||
| 
 | ||||
|     const answer04 = em.create(Answer, { | ||||
|     answer04 = em.create(Answer, { | ||||
|         author: getTestleerkracht1(), | ||||
|         toQuestion: getQuestion07(), | ||||
|         sequenceNumber: 1, | ||||
|  | @ -38,7 +37,7 @@ export function makeTestAnswers(em: EntityManager, teachers: Teacher[], question | |||
|         content: 'this is a test answer', | ||||
|     }); | ||||
| 
 | ||||
|     const answer05 = em.create(Answer, { | ||||
|     answer05 = em.create(Answer, { | ||||
|         author: getTestleerkracht1(), | ||||
|         toQuestion: getQuestion07(), | ||||
|         sequenceNumber: 2, | ||||
|  | @ -48,3 +47,29 @@ export function makeTestAnswers(em: EntityManager, teachers: Teacher[], question | |||
| 
 | ||||
|     return [answer01, answer02, answer03, answer04, answer05]; | ||||
| } | ||||
| 
 | ||||
| let answer01: Answer; | ||||
| let answer02: Answer; | ||||
| let answer03: Answer; | ||||
| let answer04: Answer; | ||||
| let answer05: Answer; | ||||
| 
 | ||||
| export function getAnswer01(): Answer { | ||||
|     return answer01; | ||||
| } | ||||
| 
 | ||||
| export function getAnswer02(): Answer { | ||||
|     return answer02; | ||||
| } | ||||
| 
 | ||||
| export function getAnswer03(): Answer { | ||||
|     return answer03; | ||||
| } | ||||
| 
 | ||||
| export function getAnswer04(): Answer { | ||||
|     return answer04; | ||||
| } | ||||
| 
 | ||||
| export function getAnswer05(): Answer { | ||||
|     return answer05; | ||||
| } | ||||
|  |  | |||
|  | @ -1,82 +1,85 @@ | |||
| import { EntityManager } from '@mikro-orm/core'; | ||||
| import { Question } from '../../../src/entities/questions/question.entity'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { Student } from '../../../src/entities/users/student.entity'; | ||||
| import { Group } from '../../../src/entities/assignments/group.entity'; | ||||
| import { getTestleerling1 } from '../users/students.testdata'; | ||||
| import { testLearningObjectMultipleChoice } from '../content/learning-objects.testdata'; | ||||
| import { getGroup1ConditionalLearningPath } from '../assignments/groups.testdata'; | ||||
| import { getDireStraits, getNoordkaap, getTestleerling1, getTool } from '../users/students.testdata'; | ||||
| import { | ||||
|     testLearningObject01, | ||||
|     testLearningObject04, | ||||
|     testLearningObject05, | ||||
|     testLearningObjectMultipleChoice, | ||||
| } from '../content/learning-objects.testdata'; | ||||
| import { getGroup1ConditionalLearningPath, getTestGroup01, getTestGroup02 } from '../assignments/groups.testdata'; | ||||
| 
 | ||||
| export function makeTestQuestions(em: EntityManager, students: Student[], groups: Group[]): Question[] { | ||||
|     const question01 = em.create(Question, { | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|         learningObjectHruid: 'id05', | ||||
|         inGroup: groups[0], // Group #1 for Assignment #1 in class 'id01'
 | ||||
| export function makeTestQuestions(em: EntityManager): Question[] { | ||||
|     question01 = em.create(Question, { | ||||
|         learningObjectLanguage: testLearningObject05.language, | ||||
|         learningObjectVersion: testLearningObject05.version, | ||||
|         learningObjectHruid: testLearningObject05.hruid, | ||||
|         inGroup: getTestGroup01(), // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         sequenceNumber: 1, | ||||
|         author: students[0], | ||||
|         author: getNoordkaap(), | ||||
|         timestamp: new Date(), | ||||
|         content: 'question', | ||||
|     }); | ||||
| 
 | ||||
|     const question02 = em.create(Question, { | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|         learningObjectHruid: 'id05', | ||||
|         inGroup: groups[0], // Group #1 for Assignment #1 in class 'id01'
 | ||||
|     question02 = em.create(Question, { | ||||
|         learningObjectLanguage: testLearningObject05.language, | ||||
|         learningObjectVersion: testLearningObject05.version, | ||||
|         learningObjectHruid: testLearningObject05.hruid, | ||||
|         inGroup: getTestGroup01(), // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         sequenceNumber: 2, | ||||
|         author: students[2], | ||||
|         author: getTool(), | ||||
|         timestamp: new Date(), | ||||
|         content: 'question', | ||||
|     }); | ||||
| 
 | ||||
|     const question03 = em.create(Question, { | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|         learningObjectHruid: 'id04', | ||||
|     //Gets deleted
 | ||||
|     question03 = em.create(Question, { | ||||
|         learningObjectLanguage: testLearningObject04.language, | ||||
|         learningObjectVersion: testLearningObject04.version, | ||||
|         learningObjectHruid: testLearningObject04.hruid, | ||||
|         sequenceNumber: 1, | ||||
|         author: students[0], | ||||
|         inGroup: groups[0], // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         author: getNoordkaap(), | ||||
|         inGroup: getTestGroup01(), // Group #1 for Assignment #1 in class 'id01'
 | ||||
|         timestamp: new Date(), | ||||
|         content: 'question', | ||||
|     }); | ||||
| 
 | ||||
|     const question04 = em.create(Question, { | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|         learningObjectHruid: 'id01', | ||||
|     question04 = em.create(Question, { | ||||
|         learningObjectLanguage: testLearningObject01.language, | ||||
|         learningObjectVersion: testLearningObject01.version, | ||||
|         learningObjectHruid: testLearningObject01.hruid, | ||||
|         sequenceNumber: 1, | ||||
|         author: students[1], | ||||
|         inGroup: groups[1], // Group #2 for Assignment #1 in class 'id01'
 | ||||
|         author: getDireStraits(), | ||||
|         inGroup: getTestGroup02(), // Group #2 for Assignment #1 in class 'id01'
 | ||||
|         timestamp: new Date(), | ||||
|         content: 'question', | ||||
|     }); | ||||
| 
 | ||||
|     const question05 = em.create(Question, { | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|         learningObjectHruid: 'id05', | ||||
|     question05 = em.create(Question, { | ||||
|         learningObjectLanguage: testLearningObject05.language, | ||||
|         learningObjectVersion: testLearningObject05.version, | ||||
|         learningObjectHruid: testLearningObject05.hruid, | ||||
|         sequenceNumber: 3, | ||||
|         author: students[1], | ||||
|         inGroup: groups[1], // Group #2 for Assignment #1 in class 'id01'
 | ||||
|         author: getDireStraits(), | ||||
|         inGroup: getTestGroup02(), // Group #2 for Assignment #1 in class 'id01'
 | ||||
|         timestamp: new Date(), | ||||
|         content: 'question', | ||||
|     }); | ||||
| 
 | ||||
|     const question06 = em.create(Question, { | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|         learningObjectHruid: 'id05', | ||||
|     question06 = em.create(Question, { | ||||
|         learningObjectLanguage: testLearningObject05.language, | ||||
|         learningObjectVersion: testLearningObject05.version, | ||||
|         learningObjectHruid: testLearningObject05.hruid, | ||||
|         sequenceNumber: 4, | ||||
|         author: students[2], | ||||
|         inGroup: groups[5], // Group #4 for Assignment #2 in class 'id02'
 | ||||
|         author: getTool(), | ||||
|         inGroup: getGroup1ConditionalLearningPath(), // Group #4 for Assignment #2 in class 'id02'
 | ||||
|         timestamp: new Date(), | ||||
|         content: 'question', | ||||
|     }); | ||||
| 
 | ||||
|     question07 = em.create(Question, { | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|         learningObjectLanguage: testLearningObjectMultipleChoice.language, | ||||
|         learningObjectVersion: testLearningObjectMultipleChoice.version, | ||||
|         learningObjectHruid: testLearningObjectMultipleChoice.hruid, | ||||
|         sequenceNumber: 1, | ||||
|         author: getTestleerling1(), | ||||
|  | @ -86,8 +89,8 @@ export function makeTestQuestions(em: EntityManager, students: Student[], groups | |||
|     }); | ||||
| 
 | ||||
|     question08 = em.create(Question, { | ||||
|         learningObjectLanguage: Language.English, | ||||
|         learningObjectVersion: 1, | ||||
|         learningObjectLanguage: testLearningObjectMultipleChoice.language, | ||||
|         learningObjectVersion: testLearningObjectMultipleChoice.version, | ||||
|         learningObjectHruid: testLearningObjectMultipleChoice.hruid, | ||||
|         sequenceNumber: 2, | ||||
|         author: getTestleerling1(), | ||||
|  | @ -99,8 +102,38 @@ export function makeTestQuestions(em: EntityManager, students: Student[], groups | |||
|     return [question01, question02, question03, question04, question05, question06, question07, question08]; | ||||
| } | ||||
| 
 | ||||
| let question08: Question; | ||||
| let question01: Question; | ||||
| let question02: Question; | ||||
| let question03: Question; | ||||
| let question04: Question; | ||||
| let question05: Question; | ||||
| let question06: Question; | ||||
| let question07: Question; | ||||
| let question08: Question; | ||||
| 
 | ||||
| export function getQuestion01(): Question { | ||||
|     return question01; | ||||
| } | ||||
| 
 | ||||
| export function getQuestion02(): Question { | ||||
|     return question02; | ||||
| } | ||||
| 
 | ||||
| export function getQuestion03(): Question { | ||||
|     return question03; | ||||
| } | ||||
| 
 | ||||
| export function getQuestion04(): Question { | ||||
|     return question04; | ||||
| } | ||||
| 
 | ||||
| export function getQuestion05(): Question { | ||||
|     return question05; | ||||
| } | ||||
| 
 | ||||
| export function getQuestion06(): Question { | ||||
|     return question06; | ||||
| } | ||||
| 
 | ||||
| export function getQuestion07(): Question { | ||||
|     return question07; | ||||
|  |  | |||
|  | @ -9,7 +9,6 @@ export const TEST_STUDENTS = [ | |||
|     { username: 'SmashingPumpkins', firstName: 'Billy', lastName: 'Corgan' }, | ||||
|     { username: 'PinkFloyd', firstName: 'David', lastName: 'Gilmoure' }, | ||||
|     { username: 'TheDoors', firstName: 'Jim', lastName: 'Morisson' }, | ||||
|     // ⚠️ Deze mag niet gebruikt worden in elke test!
 | ||||
|     { username: 'Nirvana', firstName: 'Kurt', lastName: 'Cobain' }, | ||||
|     // Makes sure when logged in as leerling1, there exists a corresponding user
 | ||||
|     { username: 'testleerling1', firstName: 'Gerald', lastName: 'Schmittinger' }, | ||||
|  | @ -24,5 +23,33 @@ export function makeTestStudents(em: EntityManager): Student[] { | |||
| } | ||||
| 
 | ||||
| export function getTestleerling1(): Student { | ||||
|     return testStudents.find((it) => it.username === 'testleerling1'); | ||||
|     return testStudents.find((it) => it.username === 'testleerling1')!; | ||||
| } | ||||
| 
 | ||||
| export function getNoordkaap(): Student { | ||||
|     return testStudents.find((it) => it.username === 'Noordkaap')!; | ||||
| } | ||||
| 
 | ||||
| export function getDireStraits(): Student { | ||||
|     return testStudents.find((it) => it.username === 'DireStraits')!; | ||||
| } | ||||
| 
 | ||||
| export function getTool(): Student { | ||||
|     return testStudents.find((it) => it.username === 'Tool')!; | ||||
| } | ||||
| 
 | ||||
| export function getSmashingPumpkins(): Student { | ||||
|     return testStudents.find((it) => it.username === 'SmashingPumpkins')!; | ||||
| } | ||||
| 
 | ||||
| export function getPinkFloyd(): Student { | ||||
|     return testStudents.find((it) => it.username === 'PinkFloyd')!; | ||||
| } | ||||
| 
 | ||||
| export function getTheDoors(): Student { | ||||
|     return testStudents.find((it) => it.username === 'TheDoors')!; | ||||
| } | ||||
| 
 | ||||
| export function getNirvana(): Student { | ||||
|     return testStudents.find((it) => it.username === 'Nirvana')!; | ||||
| } | ||||
|  |  | |||
|  | @ -43,19 +43,19 @@ let teacher03: Teacher; | |||
| let teacher04: Teacher; | ||||
| let testleerkracht1: Teacher; | ||||
| 
 | ||||
| export function getTeacher01(): Teacher { | ||||
| export function getFooFighters(): Teacher { | ||||
|     return teacher01; | ||||
| } | ||||
| 
 | ||||
| export function getTeacher02(): Teacher { | ||||
| export function getLimpBizkit(): Teacher { | ||||
|     return teacher02; | ||||
| } | ||||
| 
 | ||||
| export function getTeacher03(): Teacher { | ||||
| export function getStaind(): Teacher { | ||||
|     return teacher03; | ||||
| } | ||||
| 
 | ||||
| export function getTeacher04(): Teacher { | ||||
| export function getZesdeMetaal(): Teacher { | ||||
|     return teacher04; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,82 +1,23 @@ | |||
| import { forkEntityManager, initORM } from '../src/orm.js'; | ||||
| import { initORM } from '../src/orm.js'; | ||||
| import dotenv from 'dotenv'; | ||||
| import { makeTestAssignemnts } from '../tests/test_assets/assignments/assignments.testdata.js'; | ||||
| import { makeTestGroups } from '../tests/test_assets/assignments/groups.testdata.js'; | ||||
| import { makeTestSubmissions } from '../tests/test_assets/assignments/submission.testdata.js'; | ||||
| import { makeTestClassJoinRequests } from '../tests/test_assets/classes/class-join-requests.testdata.js'; | ||||
| import { makeTestClasses } from '../tests/test_assets/classes/classes.testdata.js'; | ||||
| import { makeTestTeacherInvitations } from '../tests/test_assets/classes/teacher-invitations.testdata.js'; | ||||
| import { makeTestAttachments } from '../tests/test_assets/content/attachments.testdata.js'; | ||||
| import { makeTestLearningObjects } from '../tests/test_assets/content/learning-objects.testdata.js'; | ||||
| import { makeTestLearningPaths } from '../tests/test_assets/content/learning-paths.testdata.js'; | ||||
| import { makeTestAnswers } from '../tests/test_assets/questions/answers.testdata.js'; | ||||
| import { makeTestQuestions } from '../tests/test_assets/questions/questions.testdata.js'; | ||||
| import { makeTestStudents } from '../tests/test_assets/users/students.testdata.js'; | ||||
| import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js'; | ||||
| import { getLogger, Logger } from '../src/logging/initalize.js'; | ||||
| import { Collection, MikroORM } from '@mikro-orm/core'; | ||||
| import { Group } from '../src/entities/assignments/group.entity'; | ||||
| import { seedORM } from './seedORM.js'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
| export async function seedORM(orm: MikroORM): Promise<void> { | ||||
|     await orm.schema.clearDatabase(); | ||||
| 
 | ||||
|     const em = forkEntityManager(); | ||||
| 
 | ||||
|     logger.info('seeding database...'); | ||||
| 
 | ||||
|     const students = makeTestStudents(em); | ||||
|     const teachers = makeTestTeachers(em); | ||||
|     const learningObjects = makeTestLearningObjects(em); | ||||
|     const learningPaths = makeTestLearningPaths(em); | ||||
|     const classes = makeTestClasses(em, students, teachers); | ||||
|     const assignments = makeTestAssignemnts(em, classes); | ||||
| 
 | ||||
|     const groups = makeTestGroups(em, students, assignments); | ||||
| 
 | ||||
|     assignments[0].groups = new Collection<Group>(groups.slice(0, 3)); | ||||
|     assignments[1].groups = new Collection<Group>(groups.slice(3, 4)); | ||||
| 
 | ||||
|     const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes); | ||||
|     const classJoinRequests = makeTestClassJoinRequests(em, students, classes); | ||||
|     const attachments = makeTestAttachments(em, learningObjects); | ||||
| 
 | ||||
|     learningObjects[1].attachments = attachments; | ||||
| 
 | ||||
|     const questions = makeTestQuestions(em, students, groups); | ||||
|     const answers = makeTestAnswers(em, teachers, questions); | ||||
|     const submissions = makeTestSubmissions(em, students, groups); | ||||
| 
 | ||||
|     // Persist all entities
 | ||||
|     await em.persistAndFlush([ | ||||
|         ...students, | ||||
|         ...teachers, | ||||
|         ...learningObjects, | ||||
|         ...learningPaths, | ||||
|         ...classes, | ||||
|         ...assignments, | ||||
|         ...groups, | ||||
|         ...teacherInvitations, | ||||
|         ...classJoinRequests, | ||||
|         ...attachments, | ||||
|         ...questions, | ||||
|         ...answers, | ||||
|         ...submissions, | ||||
|     ]); | ||||
| 
 | ||||
|     logger.info('Development database seeded successfully!'); | ||||
| } | ||||
| 
 | ||||
| export async function seedDatabase(envFile = '.env.development.local', testMode = false): Promise<void> { | ||||
| export async function seedDatabase( | ||||
|     envFile = '.env.development.local', | ||||
|     testMode = process.env.NODE_ENV !== undefined && process.env.NODE_ENV === 'test' | ||||
| ): Promise<void> { | ||||
|     dotenv.config({ path: envFile }); | ||||
|     const orm = await initORM(testMode); | ||||
| 
 | ||||
|     await seedORM(orm); | ||||
| 
 | ||||
|     await orm.close(); | ||||
|     try { | ||||
|         const orm = await initORM(testMode); | ||||
|         await seedORM(orm); | ||||
|         await orm.close(); | ||||
|     } catch (err) { | ||||
|         logger.error(`Error: ${err}`); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| seedDatabase().catch((err) => { | ||||
|     logger.error(err); | ||||
| }); | ||||
| seedDatabase().catch((err) => logger.error(`Seeding: ${err}`)); | ||||
|  |  | |||
							
								
								
									
										70
									
								
								backend/tool/seedORM.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								backend/tool/seedORM.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | |||
| import { Collection, MikroORM } from '@mikro-orm/core'; | ||||
| import { forkEntityManager } from '../src/orm'; | ||||
| import { makeTestStudents } from '../tests/test_assets/users/students.testdata'; | ||||
| import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata'; | ||||
| import { makeTestLearningObjects } from '../tests/test_assets/content/learning-objects.testdata'; | ||||
| import { makeTestLearningPaths } from '../tests/test_assets/content/learning-paths.testdata'; | ||||
| import { makeTestClasses } from '../tests/test_assets/classes/classes.testdata'; | ||||
| import { makeTestAssignemnts } from '../tests/test_assets/assignments/assignments.testdata'; | ||||
| import { getTestGroup01, getTestGroup02, getTestGroup03, getTestGroup04, makeTestGroups } from '../tests/test_assets/assignments/groups.testdata'; | ||||
| import { Group } from '../src/entities/assignments/group.entity'; | ||||
| import { makeTestTeacherInvitations } from '../tests/test_assets/classes/teacher-invitations.testdata'; | ||||
| import { makeTestClassJoinRequests } from '../tests/test_assets/classes/class-join-requests.testdata'; | ||||
| import { makeTestAttachments } from '../tests/test_assets/content/attachments.testdata'; | ||||
| import { makeTestQuestions } from '../tests/test_assets/questions/questions.testdata'; | ||||
| import { makeTestAnswers } from '../tests/test_assets/questions/answers.testdata'; | ||||
| import { makeTestSubmissions } from '../tests/test_assets/assignments/submission.testdata'; | ||||
| import { getLogger } from '../src/logging/initalize'; | ||||
| 
 | ||||
| export async function seedORM(orm: MikroORM): Promise<void> { | ||||
|     const logger = getLogger(); | ||||
| 
 | ||||
|     logger.debug('Clearing database...'); | ||||
|     await orm.schema.clearDatabase(); | ||||
| 
 | ||||
|     logger.debug('Forking entity manager...'); | ||||
|     const em = forkEntityManager(); | ||||
| 
 | ||||
|     logger.debug('Seeding database...'); | ||||
| 
 | ||||
|     const students = makeTestStudents(em); | ||||
|     const teachers = makeTestTeachers(em); | ||||
|     const learningObjects = makeTestLearningObjects(em); | ||||
|     const learningPaths = makeTestLearningPaths(em); | ||||
|     const classes = makeTestClasses(em); | ||||
|     const assignments = makeTestAssignemnts(em); | ||||
| 
 | ||||
|     const groups = makeTestGroups(em); | ||||
| 
 | ||||
|     assignments[0].groups = new Collection<Group>([getTestGroup01(), getTestGroup02(), getTestGroup03()]); | ||||
|     assignments[1].groups = new Collection<Group>([getTestGroup04()]); | ||||
| 
 | ||||
|     const teacherInvitations = makeTestTeacherInvitations(em); | ||||
|     const classJoinRequests = makeTestClassJoinRequests(em); | ||||
|     const attachments = makeTestAttachments(em); | ||||
| 
 | ||||
|     learningObjects[1].attachments = attachments; | ||||
| 
 | ||||
|     const questions = makeTestQuestions(em); | ||||
|     const answers = makeTestAnswers(em); | ||||
|     const submissions = makeTestSubmissions(em); | ||||
| 
 | ||||
|     // Persist all entities
 | ||||
|     await em.persistAndFlush([ | ||||
|         ...students, | ||||
|         ...teachers, | ||||
|         ...learningObjects, | ||||
|         ...learningPaths, | ||||
|         ...classes, | ||||
|         ...assignments, | ||||
|         ...groups, | ||||
|         ...teacherInvitations, | ||||
|         ...classJoinRequests, | ||||
|         ...attachments, | ||||
|         ...questions, | ||||
|         ...answers, | ||||
|         ...submissions, | ||||
|     ]); | ||||
| 
 | ||||
|     logger.info('Development database seeded successfully!'); | ||||
| } | ||||
|  | @ -5,7 +5,7 @@ import { errorHandler } from '../src/middleware/error-handling/error-handler.js' | |||
| import dotenv from 'dotenv'; | ||||
| import cors from '../src/middleware/cors'; | ||||
| import { authenticateUser } from '../src/middleware/auth/auth'; | ||||
| import { seedORM } from './seed'; | ||||
| import { seedORM } from './seedORM'; | ||||
| 
 | ||||
| const envFile = '../.env.test'; | ||||
| 
 | ||||
|  |  | |||
|  | @ -157,6 +157,17 @@ services: | |||
|         volumes: | ||||
|             - dwengo_grafana_data:/var/lib/grafana | ||||
| 
 | ||||
|     caching: | ||||
|         image: memcached | ||||
|         restart: always | ||||
|         ports: | ||||
|             - '11211:11211' | ||||
|         command: | ||||
|             - --conn-limit=1024 | ||||
|             - --memory-limit=2048 | ||||
|             - -I 128m | ||||
|             - --threads=4 | ||||
| 
 | ||||
| volumes: | ||||
|     dwengo_grafana_data: | ||||
|     dwengo_letsencrypt: | ||||
|  |  | |||
|  | @ -97,6 +97,17 @@ services: | |||
|             - ./config/grafana/grafana.ini:/etc/grafana/grafana.ini | ||||
|         restart: unless-stopped | ||||
| 
 | ||||
|     caching: | ||||
|         image: memcached | ||||
|         restart: always | ||||
|         ports: | ||||
|             - '11211:11211' | ||||
|         command: | ||||
|             - --conn-limit=1024 | ||||
|             - --memory-limit=2048 | ||||
|             - -I 128m | ||||
|             - --threads=4 | ||||
| 
 | ||||
| volumes: | ||||
|     dwengo_grafana_data: | ||||
|     dwengo_loki_data: | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ export class AnswerController extends BaseController { | |||
| 
 | ||||
|     constructor(questionId: QuestionId) { | ||||
|         super( | ||||
|             `learningObject/${questionId.learningObjectIdentifier.hruid}/:${questionId.learningObjectIdentifier.version}/questions/${questionId.sequenceNumber}/answers`, | ||||
|             `learningObject/${questionId.learningObjectIdentifier.hruid}/${questionId.learningObjectIdentifier.version}/questions/${questionId.sequenceNumber}/answers`, | ||||
|         ); | ||||
|         this.loId = questionId.learningObjectIdentifier; | ||||
|         this.sequenceNumber = questionId.sequenceNumber; | ||||
|  |  | |||
							
								
								
									
										31
									
								
								frontend/tests/controllers/answers-controller.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								frontend/tests/controllers/answers-controller.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| import { describe, it, expect, beforeEach } from "vitest"; | ||||
| import { AnswerController } from "../../src/controllers/answers"; | ||||
| import { Language } from "@dwengo-1/common/util/language"; | ||||
| 
 | ||||
| describe("AnswerController Tests", () => { | ||||
|     let controller: AnswerController; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         const loiDTO = { | ||||
|             hruid: "u_test_multiple_choice", | ||||
|             language: Language.English, | ||||
|             version: 1, | ||||
|         }; | ||||
|         const questionId = { learningObjectIdentifier: loiDTO, sequenceNumber: 1 }; | ||||
|         controller = new AnswerController(questionId); | ||||
|     }); | ||||
| 
 | ||||
|     it("should fetch all answers", async () => { | ||||
|         const result = await controller.getAll(true); | ||||
|         expect(result).toHaveProperty("answers"); | ||||
|         expect(Array.isArray(result.answers)).toBe(true); | ||||
|         expect(result.answers.length).toBeGreaterThan(0); | ||||
|     }); | ||||
| 
 | ||||
|     it("should fetch an answer by sequencenumber", async () => { | ||||
|         const answerNumber = 1; // Example sequence number
 | ||||
|         const result = await controller.getBy(answerNumber); | ||||
|         expect(result).toHaveProperty("answer"); | ||||
|         expect(result.answer).toHaveProperty("sequenceNumber", answerNumber); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										30
									
								
								frontend/tests/controllers/questions-controller.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/tests/controllers/questions-controller.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| import { describe, it, expect, beforeEach } from "vitest"; | ||||
| import { Language } from "@dwengo-1/common/util/language"; | ||||
| import { QuestionController } from "../../src/controllers/questions"; | ||||
| 
 | ||||
| describe("QuestionController Tests", () => { | ||||
|     let controller: QuestionController; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         const loiDTO = { | ||||
|             hruid: "u_test_multiple_choice", | ||||
|             language: Language.English, | ||||
|             version: 1, | ||||
|         }; | ||||
|         controller = new QuestionController(loiDTO); | ||||
|     }); | ||||
| 
 | ||||
|     it("should fetch all questions", async () => { | ||||
|         const result = await controller.getAll(true); | ||||
|         expect(result).toHaveProperty("questions"); | ||||
|         expect(Array.isArray(result.questions)).toBe(true); | ||||
|         expect(result.questions.length).toBeGreaterThan(0); | ||||
|     }); | ||||
| 
 | ||||
|     it("should fetch an question by sequencenumber", async () => { | ||||
|         const questionNumber = 1; // Example sequence number
 | ||||
|         const result = await controller.getBy(questionNumber); | ||||
|         expect(result).toHaveProperty("question"); | ||||
|         expect(result.question).toHaveProperty("sequenceNumber", questionNumber); | ||||
|     }); | ||||
| }); | ||||
|  | @ -27,13 +27,8 @@ test.each([ | |||
|     { username: "DireStraits", firstName: "Mark", lastName: "Knopfler" }, | ||||
|     { username: "Tool", firstName: "Maynard", lastName: "Keenan" }, | ||||
|     { username: "SmashingPumpkins", firstName: "Billy", lastName: "Corgan" }, | ||||
|     { username: "PinkFloyd", firstName: "David", lastName: "Gilmoure" }, | ||||
|     { username: "TheDoors", firstName: "Jim", lastName: "Morisson" }, | ||||
|     // ⚠️ Deze mag niet gebruikt worden in elke test!
 | ||||
|     { username: "Nirvana", firstName: "Kurt", lastName: "Cobain" }, | ||||
|     // Makes sure when logged in as leerling1, there exists a corresponding user
 | ||||
|     { username: "testleerling1", firstName: "Gerald", lastName: "Schmittinger" }, | ||||
| ])("Get classes of student", async (student) => { | ||||
|     const data = await controller.getClasses(student.username, true); | ||||
|     expect(data.classes).to.have.length.greaterThan(0); | ||||
|     expect(data.classes).to.have.length.greaterThan(0, `Found no classes for ${student.username}`); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,15 +0,0 @@ | |||
| import { describe, expect, it } from "vitest"; | ||||
| import { SubmissionController } from "../../src/controllers/submissions"; | ||||
| import { Language } from "../../src/data-objects/language"; | ||||
| 
 | ||||
| describe("Test controller submissions", () => { | ||||
|     it("Get submission by number", async () => { | ||||
|         const hruid = "id03"; | ||||
|         const classId = "X2J9QT"; // Class01
 | ||||
|         const controller = new SubmissionController(hruid); | ||||
| 
 | ||||
|         const data = await controller.getByNumber(Language.English, 1, classId, 1, 1, 1); | ||||
| 
 | ||||
|         expect(data.submission).to.have.property("submissionNumber"); | ||||
|     }); | ||||
| }); | ||||
|  | @ -1,5 +1,6 @@ | |||
| import { spawn } from "child_process"; | ||||
| import { ChildProcess, spawnSync } from "node:child_process"; | ||||
| import { getLogger } from "../../backend/src/logging/initalize"; | ||||
| 
 | ||||
| let backendProcess: ChildProcess; | ||||
| 
 | ||||
|  | @ -35,6 +36,8 @@ export async function setup(): Promise<void> { | |||
| 
 | ||||
| export async function teardown(): Promise<void> { | ||||
|     if (backendProcess) { | ||||
|         backendProcess.kill(); | ||||
|         while (!backendProcess.kill()) { | ||||
|             getLogger().error(`Failed to kill backend process! Retrying...`); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										389
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										389
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -51,6 +51,7 @@ | |||
|                 "jwks-rsa": "^3.1.0", | ||||
|                 "loki-logger-ts": "^1.0.2", | ||||
|                 "marked": "^15.0.7", | ||||
|                 "memjs": "^1.3.2", | ||||
|                 "mime-types": "^3.0.1", | ||||
|                 "nanoid": "^5.1.5", | ||||
|                 "response-time": "^2.3.3", | ||||
|  | @ -66,6 +67,7 @@ | |||
|                 "@types/express": "^5.0.0", | ||||
|                 "@types/express-fileupload": "^1.5.1", | ||||
|                 "@types/js-yaml": "^4.0.9", | ||||
|                 "@types/memjs": "^1.3.3", | ||||
|                 "@types/mime-types": "^2.1.4", | ||||
|                 "@types/node": "^22.13.4", | ||||
|                 "@types/response-time": "^2.3.8", | ||||
|  | @ -78,6 +80,19 @@ | |||
|                 "vitest": "^3.0.6" | ||||
|             } | ||||
|         }, | ||||
|         "backend/node_modules/globals": { | ||||
|             "version": "15.15.0", | ||||
|             "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", | ||||
|             "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">=18" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "common": { | ||||
|             "name": "@dwengo-1/common", | ||||
|             "version": "0.2.0" | ||||
|  | @ -753,9 +768,9 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@codemirror/search": { | ||||
|             "version": "6.5.10", | ||||
|             "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz", | ||||
|             "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==", | ||||
|             "version": "6.5.11", | ||||
|             "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", | ||||
|             "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@codemirror/state": "^6.0.0", | ||||
|  | @ -1492,9 +1507,9 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@eslint/core": { | ||||
|             "version": "0.13.0", | ||||
|             "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", | ||||
|             "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", | ||||
|             "version": "0.14.0", | ||||
|             "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", | ||||
|             "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", | ||||
|             "dev": true, | ||||
|             "license": "Apache-2.0", | ||||
|             "dependencies": { | ||||
|  | @ -1539,19 +1554,6 @@ | |||
|                 "concat-map": "0.0.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@eslint/eslintrc/node_modules/globals": { | ||||
|             "version": "14.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", | ||||
|             "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">=18" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@eslint/eslintrc/node_modules/ignore": { | ||||
|             "version": "5.3.2", | ||||
|             "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", | ||||
|  | @ -1576,13 +1578,16 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@eslint/js": { | ||||
|             "version": "9.26.0", | ||||
|             "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", | ||||
|             "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", | ||||
|             "version": "9.27.0", | ||||
|             "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", | ||||
|             "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://eslint.org/donate" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@eslint/object-schema": { | ||||
|  | @ -1596,13 +1601,13 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@eslint/plugin-kit": { | ||||
|             "version": "0.2.8", | ||||
|             "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", | ||||
|             "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", | ||||
|             "version": "0.3.1", | ||||
|             "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", | ||||
|             "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", | ||||
|             "dev": true, | ||||
|             "license": "Apache-2.0", | ||||
|             "dependencies": { | ||||
|                 "@eslint/core": "^0.13.0", | ||||
|                 "@eslint/core": "^0.14.0", | ||||
|                 "levn": "^0.4.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|  | @ -2075,28 +2080,6 @@ | |||
|                 "@mikro-orm/core": "^6.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@modelcontextprotocol/sdk": { | ||||
|             "version": "1.11.2", | ||||
|             "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", | ||||
|             "integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "content-type": "^1.0.5", | ||||
|                 "cors": "^2.8.5", | ||||
|                 "cross-spawn": "^7.0.3", | ||||
|                 "eventsource": "^3.0.2", | ||||
|                 "express": "^5.0.1", | ||||
|                 "express-rate-limit": "^7.5.0", | ||||
|                 "pkce-challenge": "^5.0.0", | ||||
|                 "raw-body": "^3.0.0", | ||||
|                 "zod": "^3.23.8", | ||||
|                 "zod-to-json-schema": "^3.24.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=18" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@napi-rs/snappy-android-arm-eabi": { | ||||
|             "version": "7.2.2", | ||||
|             "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.2.2.tgz", | ||||
|  | @ -3107,6 +3090,16 @@ | |||
|                 "@types/node": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/memjs": { | ||||
|             "version": "1.3.3", | ||||
|             "resolved": "https://registry.npmjs.org/@types/memjs/-/memjs-1.3.3.tgz", | ||||
|             "integrity": "sha512-OqZuZ2T0ErrygA/WUqQjmsB1GpxAt84OCgKPKbsSWwYKA/oe/lTiqvqMgFOv/Ngl8p+nytgdhrdxJtikOgimCg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/node": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/mime": { | ||||
|             "version": "1.3.5", | ||||
|             "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", | ||||
|  | @ -3127,9 +3120,9 @@ | |||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/node": { | ||||
|             "version": "22.15.17", | ||||
|             "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", | ||||
|             "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", | ||||
|             "version": "22.15.18", | ||||
|             "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", | ||||
|             "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "undici-types": "~6.21.0" | ||||
|  | @ -3611,30 +3604,30 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@volar/language-core": { | ||||
|             "version": "2.4.13", | ||||
|             "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.13.tgz", | ||||
|             "integrity": "sha512-MnQJ7eKchJx5Oz+YdbqyFUk8BN6jasdJv31n/7r6/WwlOOv7qzvot6B66887l2ST3bUW4Mewml54euzpJWA6bg==", | ||||
|             "version": "2.4.14", | ||||
|             "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.14.tgz", | ||||
|             "integrity": "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@volar/source-map": "2.4.13" | ||||
|                 "@volar/source-map": "2.4.14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@volar/source-map": { | ||||
|             "version": "2.4.13", | ||||
|             "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.13.tgz", | ||||
|             "integrity": "sha512-l/EBcc2FkvHgz2ZxV+OZK3kMSroMr7nN3sZLF2/f6kWW66q8+tEL4giiYyFjt0BcubqJhBt6soYIrAPhg/Yr+Q==", | ||||
|             "version": "2.4.14", | ||||
|             "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.14.tgz", | ||||
|             "integrity": "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@volar/typescript": { | ||||
|             "version": "2.4.13", | ||||
|             "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.13.tgz", | ||||
|             "integrity": "sha512-Ukz4xv84swJPupZeoFsQoeJEOm7U9pqsEnaGGgt5ni3SCTa22m8oJP5Nng3Wed7Uw5RBELdLxxORX8YhJPyOgQ==", | ||||
|             "version": "2.4.14", | ||||
|             "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.14.tgz", | ||||
|             "integrity": "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@volar/language-core": "2.4.13", | ||||
|                 "@volar/language-core": "2.4.14", | ||||
|                 "path-browserify": "^1.0.1", | ||||
|                 "vscode-uri": "^3.0.8" | ||||
|             } | ||||
|  | @ -3693,16 +3686,16 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/compiler-core": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", | ||||
|             "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.14.tgz", | ||||
|             "integrity": "sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@babel/parser": "^7.25.3", | ||||
|                 "@vue/shared": "3.5.13", | ||||
|                 "@babel/parser": "^7.27.2", | ||||
|                 "@vue/shared": "3.5.14", | ||||
|                 "entities": "^4.5.0", | ||||
|                 "estree-walker": "^2.0.2", | ||||
|                 "source-map-js": "^1.2.0" | ||||
|                 "source-map-js": "^1.2.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/compiler-core/node_modules/entities": { | ||||
|  | @ -3718,40 +3711,40 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/compiler-dom": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", | ||||
|             "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.14.tgz", | ||||
|             "integrity": "sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@vue/compiler-core": "3.5.13", | ||||
|                 "@vue/shared": "3.5.13" | ||||
|                 "@vue/compiler-core": "3.5.14", | ||||
|                 "@vue/shared": "3.5.14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/compiler-sfc": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", | ||||
|             "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.14.tgz", | ||||
|             "integrity": "sha512-9T6m/9mMr81Lj58JpzsiSIjBgv2LiVoWjIVa7kuXHICUi8LiDSIotMpPRXYJsXKqyARrzjT24NAwttrMnMaCXA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@babel/parser": "^7.25.3", | ||||
|                 "@vue/compiler-core": "3.5.13", | ||||
|                 "@vue/compiler-dom": "3.5.13", | ||||
|                 "@vue/compiler-ssr": "3.5.13", | ||||
|                 "@vue/shared": "3.5.13", | ||||
|                 "@babel/parser": "^7.27.2", | ||||
|                 "@vue/compiler-core": "3.5.14", | ||||
|                 "@vue/compiler-dom": "3.5.14", | ||||
|                 "@vue/compiler-ssr": "3.5.14", | ||||
|                 "@vue/shared": "3.5.14", | ||||
|                 "estree-walker": "^2.0.2", | ||||
|                 "magic-string": "^0.30.11", | ||||
|                 "postcss": "^8.4.48", | ||||
|                 "source-map-js": "^1.2.0" | ||||
|                 "magic-string": "^0.30.17", | ||||
|                 "postcss": "^8.5.3", | ||||
|                 "source-map-js": "^1.2.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/compiler-ssr": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", | ||||
|             "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.14.tgz", | ||||
|             "integrity": "sha512-Y0G7PcBxr1yllnHuS/NxNCSPWnRGH4Ogrp0tsLA5QemDZuJLs99YjAKQ7KqkHE0vCg4QTKlQzXLKCMF7WPSl7Q==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@vue/compiler-dom": "3.5.13", | ||||
|                 "@vue/shared": "3.5.13" | ||||
|                 "@vue/compiler-dom": "3.5.14", | ||||
|                 "@vue/shared": "3.5.14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/compiler-vue2": { | ||||
|  | @ -3882,53 +3875,53 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/reactivity": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", | ||||
|             "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz", | ||||
|             "integrity": "sha512-7cK1Hp343Fu/SUCCO52vCabjvsYu7ZkOqyYu7bXV9P2yyfjUMUXHZafEbq244sP7gf+EZEz+77QixBTuEqkQQw==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@vue/shared": "3.5.13" | ||||
|                 "@vue/shared": "3.5.14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/runtime-core": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", | ||||
|             "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.14.tgz", | ||||
|             "integrity": "sha512-w9JWEANwHXNgieAhxPpEpJa+0V5G0hz3NmjAZwlOebtfKyp2hKxKF0+qSh0Xs6/PhfGihuSdqMprMVcQU/E6ag==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@vue/reactivity": "3.5.13", | ||||
|                 "@vue/shared": "3.5.13" | ||||
|                 "@vue/reactivity": "3.5.14", | ||||
|                 "@vue/shared": "3.5.14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/runtime-dom": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", | ||||
|             "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.14.tgz", | ||||
|             "integrity": "sha512-lCfR++IakeI35TVR80QgOelsUIdcKjd65rWAMfdSlCYnaEY5t3hYwru7vvcWaqmrK+LpI7ZDDYiGU5V3xjMacw==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@vue/reactivity": "3.5.13", | ||||
|                 "@vue/runtime-core": "3.5.13", | ||||
|                 "@vue/shared": "3.5.13", | ||||
|                 "@vue/reactivity": "3.5.14", | ||||
|                 "@vue/runtime-core": "3.5.14", | ||||
|                 "@vue/shared": "3.5.14", | ||||
|                 "csstype": "^3.1.3" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/server-renderer": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", | ||||
|             "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.14.tgz", | ||||
|             "integrity": "sha512-Rf/ISLqokIvcySIYnv3tNWq40PLpNLDLSJwwVWzG6MNtyIhfbcrAxo5ZL9nARJhqjZyWWa40oRb2IDuejeuv6w==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@vue/compiler-ssr": "3.5.13", | ||||
|                 "@vue/shared": "3.5.13" | ||||
|                 "@vue/compiler-ssr": "3.5.14", | ||||
|                 "@vue/shared": "3.5.14" | ||||
|             }, | ||||
|             "peerDependencies": { | ||||
|                 "vue": "3.5.13" | ||||
|                 "vue": "3.5.14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@vue/shared": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", | ||||
|             "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.14.tgz", | ||||
|             "integrity": "sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@vue/test-utils": { | ||||
|  | @ -3962,14 +3955,14 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@vueuse/core": { | ||||
|             "version": "13.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.1.0.tgz", | ||||
|             "integrity": "sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==", | ||||
|             "version": "13.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.2.0.tgz", | ||||
|             "integrity": "sha512-n5TZoIAxbWAQ3PqdVPDzLgIRQOujFfMlatdI+f7ditSmoEeNpPBvp7h2zamzikCmrhFIePAwdEQB6ENccHr7Rg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/web-bluetooth": "^0.0.21", | ||||
|                 "@vueuse/metadata": "13.1.0", | ||||
|                 "@vueuse/shared": "13.1.0" | ||||
|                 "@vueuse/metadata": "13.2.0", | ||||
|                 "@vueuse/shared": "13.2.0" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/antfu" | ||||
|  | @ -3979,18 +3972,18 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/@vueuse/metadata": { | ||||
|             "version": "13.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.1.0.tgz", | ||||
|             "integrity": "sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==", | ||||
|             "version": "13.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.2.0.tgz", | ||||
|             "integrity": "sha512-kPpzuQCU0+D8DZCzK0iPpIcXI+6ufWSgwnjJ6//GNpEn+SHViaCtR+XurzORChSgvpHO9YC8gGM97Y1kB+UabA==", | ||||
|             "license": "MIT", | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/antfu" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@vueuse/shared": { | ||||
|             "version": "13.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.1.0.tgz", | ||||
|             "integrity": "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==", | ||||
|             "version": "13.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.2.0.tgz", | ||||
|             "integrity": "sha512-vx9ZPDF5HcU9up3Jgt3G62dMUfZEdk6tLyBAHYAG4F4n73vpaA7J5hdncDI/lS9Vm7GA/FPlbOmh9TrDZROTpg==", | ||||
|             "license": "MIT", | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/antfu" | ||||
|  | @ -5448,9 +5441,9 @@ | |||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/electron-to-chromium": { | ||||
|             "version": "1.5.152", | ||||
|             "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz", | ||||
|             "integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==", | ||||
|             "version": "1.5.155", | ||||
|             "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", | ||||
|             "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", | ||||
|             "dev": true, | ||||
|             "license": "ISC" | ||||
|         }, | ||||
|  | @ -5656,9 +5649,9 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/eslint": { | ||||
|             "version": "9.26.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", | ||||
|             "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", | ||||
|             "version": "9.27.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", | ||||
|             "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|  | @ -5666,14 +5659,13 @@ | |||
|                 "@eslint-community/regexpp": "^4.12.1", | ||||
|                 "@eslint/config-array": "^0.20.0", | ||||
|                 "@eslint/config-helpers": "^0.2.1", | ||||
|                 "@eslint/core": "^0.13.0", | ||||
|                 "@eslint/core": "^0.14.0", | ||||
|                 "@eslint/eslintrc": "^3.3.1", | ||||
|                 "@eslint/js": "9.26.0", | ||||
|                 "@eslint/plugin-kit": "^0.2.8", | ||||
|                 "@eslint/js": "9.27.0", | ||||
|                 "@eslint/plugin-kit": "^0.3.1", | ||||
|                 "@humanfs/node": "^0.16.6", | ||||
|                 "@humanwhocodes/module-importer": "^1.0.1", | ||||
|                 "@humanwhocodes/retry": "^0.4.2", | ||||
|                 "@modelcontextprotocol/sdk": "^1.8.0", | ||||
|                 "@types/estree": "^1.0.6", | ||||
|                 "@types/json-schema": "^7.0.15", | ||||
|                 "ajv": "^6.12.4", | ||||
|  | @ -5697,8 +5689,7 @@ | |||
|                 "lodash.merge": "^4.6.2", | ||||
|                 "minimatch": "^3.1.2", | ||||
|                 "natural-compare": "^1.4.0", | ||||
|                 "optionator": "^0.9.3", | ||||
|                 "zod": "^3.24.2" | ||||
|                 "optionator": "^0.9.3" | ||||
|             }, | ||||
|             "bin": { | ||||
|                 "eslint": "bin/eslint.js" | ||||
|  | @ -6105,29 +6096,6 @@ | |||
|                 "node": ">= 0.6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/eventsource": { | ||||
|             "version": "3.0.7", | ||||
|             "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", | ||||
|             "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "eventsource-parser": "^3.0.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=18.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/eventsource-parser": { | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", | ||||
|             "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">=18.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/execa": { | ||||
|             "version": "9.5.3", | ||||
|             "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.3.tgz", | ||||
|  | @ -6242,22 +6210,6 @@ | |||
|                 "node": ">= 8.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/express-rate-limit": { | ||||
|             "version": "7.5.0", | ||||
|             "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", | ||||
|             "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">= 16" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/express-rate-limit" | ||||
|             }, | ||||
|             "peerDependencies": { | ||||
|                 "express": "^4.11 || 5 || ^5.0.0-beta.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/express-unless": { | ||||
|             "version": "2.1.3", | ||||
|             "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", | ||||
|  | @ -6861,9 +6813,9 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/globals": { | ||||
|             "version": "15.15.0", | ||||
|             "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", | ||||
|             "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", | ||||
|             "version": "14.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", | ||||
|             "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|  | @ -8354,6 +8306,15 @@ | |||
|                 "node": ">= 0.8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/memjs": { | ||||
|             "version": "1.3.2", | ||||
|             "resolved": "https://registry.npmjs.org/memjs/-/memjs-1.3.2.tgz", | ||||
|             "integrity": "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg==", | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">=0.10.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/memoize-one": { | ||||
|             "version": "6.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", | ||||
|  | @ -9504,16 +9465,6 @@ | |||
|                 "node": ">=0.10" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/pkce-challenge": { | ||||
|             "version": "5.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", | ||||
|             "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">=16.20.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/playwright": { | ||||
|             "version": "1.50.1", | ||||
|             "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", | ||||
|  | @ -9759,9 +9710,9 @@ | |||
|             "license": "ISC" | ||||
|         }, | ||||
|         "node_modules/protobufjs": { | ||||
|             "version": "7.5.1", | ||||
|             "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.1.tgz", | ||||
|             "integrity": "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw==", | ||||
|             "version": "7.5.2", | ||||
|             "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz", | ||||
|             "integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==", | ||||
|             "hasInstallScript": true, | ||||
|             "license": "BSD-3-Clause", | ||||
|             "dependencies": { | ||||
|  | @ -10966,9 +10917,9 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/svelte": { | ||||
|             "version": "5.28.6", | ||||
|             "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.6.tgz", | ||||
|             "integrity": "sha512-9qqr7mw8YR9PAnxGFfzCK6PUlNGtns7wVavrhnxyf3fpB1mP/Ol55Z2UnIapsSzNNl3k9qw7cZ22PdE8+xT/jQ==", | ||||
|             "version": "5.30.1", | ||||
|             "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz", | ||||
|             "integrity": "sha512-QIYtKnJGkubWXtNkrUBKVCvyo9gjcccdbnvXfwsGNhvbeNNdQjRDTa/BiQcJ2kWXbXPQbWKyT7CUu53KIj1rfw==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@ampproject/remapping": "^2.3.0", | ||||
|  | @ -11093,13 +11044,13 @@ | |||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/synckit": { | ||||
|             "version": "0.11.4", | ||||
|             "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", | ||||
|             "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", | ||||
|             "version": "0.11.5", | ||||
|             "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.5.tgz", | ||||
|             "integrity": "sha512-frqvfWyDA5VPVdrWfH24uM6SI/O8NLpVbIIJxb8t/a3YGsp4AW9CYgSKC0OaSEfexnp7Y1pVh2Y6IHO8ggGDmA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@pkgr/core": "^0.2.3", | ||||
|                 "@pkgr/core": "^0.2.4", | ||||
|                 "tslib": "^2.8.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|  | @ -12118,16 +12069,16 @@ | |||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/vue": { | ||||
|             "version": "3.5.13", | ||||
|             "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", | ||||
|             "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", | ||||
|             "version": "3.5.14", | ||||
|             "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.14.tgz", | ||||
|             "integrity": "sha512-LbOm50/vZFG6Mhy6KscQYXZMQ0LMCC/y40HDJPPvGFQ+i/lUH+PJHR6C3assgOQiXdl6tAfsXHbXYVBZZu65ew==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@vue/compiler-dom": "3.5.13", | ||||
|                 "@vue/compiler-sfc": "3.5.13", | ||||
|                 "@vue/runtime-dom": "3.5.13", | ||||
|                 "@vue/server-renderer": "3.5.13", | ||||
|                 "@vue/shared": "3.5.13" | ||||
|                 "@vue/compiler-dom": "3.5.14", | ||||
|                 "@vue/compiler-sfc": "3.5.14", | ||||
|                 "@vue/runtime-dom": "3.5.14", | ||||
|                 "@vue/server-renderer": "3.5.14", | ||||
|                 "@vue/shared": "3.5.14" | ||||
|             }, | ||||
|             "peerDependencies": { | ||||
|                 "typescript": "*" | ||||
|  | @ -12262,9 +12213,9 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/vuetify": { | ||||
|             "version": "3.8.4", | ||||
|             "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.8.4.tgz", | ||||
|             "integrity": "sha512-hfA1eqA6vhrF5LF8Yfk0uHdNUmh8Uckxn5wREiThO82HW/9Vfreh+IpxPgEtCsAhV33KW+NVamltQCu3HczRKw==", | ||||
|             "version": "3.8.5", | ||||
|             "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.8.5.tgz", | ||||
|             "integrity": "sha512-W/mTaNDyO6NRqAQmnkMUn9TYvRb//BPF/vk7h3+2xNJOyI9ev90JmYjrihOtb+6QDrB79wVUH0Y+0OjYK73GsA==", | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": "^12.20 || >=14.13" | ||||
|  | @ -12819,26 +12770,6 @@ | |||
|             "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", | ||||
|             "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/zod": { | ||||
|             "version": "3.24.4", | ||||
|             "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", | ||||
|             "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/colinhacks" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/zod-to-json-schema": { | ||||
|             "version": "3.24.5", | ||||
|             "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", | ||||
|             "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", | ||||
|             "dev": true, | ||||
|             "license": "ISC", | ||||
|             "peerDependencies": { | ||||
|                 "zod": "^3.24.1" | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana