@citolab/qti-backend-firebase 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/app-specific/base/application-specific-base.d.ts +11 -20
- package/dist/api/app-specific/base/application-specific-base.d.ts.map +1 -1
- package/dist/api/app-specific/base/application-specific-base.js +73 -88
- package/dist/api/app-specific/base/application-specific-base.js.map +1 -1
- package/dist/api/app-specific/baseImplementation.d.ts +2 -3
- package/dist/api/app-specific/baseImplementation.d.ts.map +1 -1
- package/dist/api/app-specific/baseImplementation.js +2 -6
- package/dist/api/app-specific/baseImplementation.js.map +1 -1
- package/dist/api/app-specific/index.d.ts +1 -0
- package/dist/api/app-specific/index.d.ts.map +1 -1
- package/dist/api/app-specific/index.js +1 -0
- package/dist/api/app-specific/index.js.map +1 -1
- package/dist/api/app-specific/interface/IApplicationSpecific.d.ts +10 -20
- package/dist/api/app-specific/interface/IApplicationSpecific.d.ts.map +1 -1
- package/dist/api/qti-data.d.ts.map +1 -1
- package/dist/api/qti-data.js +619 -271
- package/dist/api/qti-data.js.map +1 -1
- package/dist/api/qti-resources.d.ts.map +1 -1
- package/dist/api/qti-resources.js +134 -131
- package/dist/api/qti-resources.js.map +1 -1
- package/dist/api/qti-teacher.d.ts.map +1 -1
- package/dist/api/qti-teacher.js +563 -424
- package/dist/api/qti-teacher.js.map +1 -1
- package/dist/api/qti-tools.d.ts.map +1 -1
- package/dist/api/qti-tools.js +213 -41
- package/dist/api/qti-tools.js.map +1 -1
- package/dist/helpers/database.d.ts +35 -24
- package/dist/helpers/database.d.ts.map +1 -1
- package/dist/helpers/database.js +47 -36
- package/dist/helpers/database.js.map +1 -1
- package/dist/helpers/db.d.ts +1 -1
- package/dist/helpers/db.d.ts.map +1 -1
- package/dist/helpers/db.js +3 -7
- package/dist/helpers/db.js.map +1 -1
- package/dist/helpers/endpoint-helpers.d.ts +2 -2
- package/dist/helpers/endpoint-helpers.d.ts.map +1 -1
- package/dist/helpers/endpoint-helpers.js +59 -17
- package/dist/helpers/endpoint-helpers.js.map +1 -1
- package/dist/helpers/excel-helper.d.ts +29 -0
- package/dist/helpers/excel-helper.d.ts.map +1 -0
- package/dist/helpers/excel-helper.js +150 -0
- package/dist/helpers/excel-helper.js.map +1 -0
- package/dist/helpers/general.d.ts +15 -0
- package/dist/helpers/general.d.ts.map +1 -0
- package/dist/helpers/general.js +148 -0
- package/dist/helpers/general.js.map +1 -0
- package/dist/helpers/index.d.ts +2 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +2 -0
- package/dist/helpers/index.js.map +1 -1
- package/dist/helpers/logic.d.ts +100 -41
- package/dist/helpers/logic.d.ts.map +1 -1
- package/dist/helpers/logic.js +523 -413
- package/dist/helpers/logic.js.map +1 -1
- package/dist/helpers/package-upload.d.ts.map +1 -1
- package/dist/helpers/package-upload.js +2 -3
- package/dist/helpers/package-upload.js.map +1 -1
- package/dist/helpers/package.d.ts +3 -4
- package/dist/helpers/package.d.ts.map +1 -1
- package/dist/helpers/package.js +12 -17
- package/dist/helpers/package.js.map +1 -1
- package/dist/helpers/request-headers.d.ts +4 -0
- package/dist/helpers/request-headers.d.ts.map +1 -1
- package/dist/helpers/request-headers.js +2 -2
- package/dist/helpers/request-headers.js.map +1 -1
- package/dist/helpers/resource-cache.d.ts +35 -0
- package/dist/helpers/resource-cache.d.ts.map +1 -0
- package/dist/helpers/resource-cache.js +171 -0
- package/dist/helpers/resource-cache.js.map +1 -0
- package/dist/helpers/storage.d.ts +2 -2
- package/dist/helpers/storage.d.ts.map +1 -1
- package/dist/helpers/storage.js +16 -15
- package/dist/helpers/storage.js.map +1 -1
- package/dist/helpers/utils.d.ts +4 -3
- package/dist/helpers/utils.d.ts.map +1 -1
- package/dist/helpers/utils.js +73 -51
- package/dist/helpers/utils.js.map +1 -1
- package/package.json +12 -9
package/dist/api/qti-data.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSessionForDelivery, getDelivery, getSession, getTestsetSessions, getTestsetSessionWithSessions, updateSessionState, processFeedbackSubmission, validateFeedbackData, saveFeedback, } from "./../helpers/logic";
|
|
2
2
|
import { dateId } from "./../helpers/utils";
|
|
3
|
-
import {
|
|
3
|
+
import { Router } from "express";
|
|
4
4
|
import NodeCache from "node-cache";
|
|
5
|
-
import {
|
|
6
|
-
import { getFirestore, getStorage } from "../helpers/firebase";
|
|
5
|
+
import { getFirestore } from "../helpers/firebase";
|
|
7
6
|
import { body, param } from "express-validator";
|
|
8
|
-
import { errorHandler, generalRateLimit, handleValidationErrors,
|
|
7
|
+
import { errorHandler, generalRateLimit, handleValidationErrors, requireAuth, requireAuthType, sendError, sendSuccess, } from "../helpers/endpoint-helpers";
|
|
9
8
|
import { BaseImplementation } from "./app-specific/baseImplementation";
|
|
9
|
+
import { getDatabase } from "../helpers";
|
|
10
10
|
export function addQtiDataEndpoints(app, specificImplementation = new BaseImplementation()) {
|
|
11
|
+
const dataRouter = Router();
|
|
11
12
|
const cache = new NodeCache({
|
|
12
13
|
stdTTL: 300, // 5 minutes
|
|
13
14
|
checkperiod: 60, // Check for expired keys every minute
|
|
14
15
|
});
|
|
15
16
|
// Apply global error handler
|
|
16
|
-
|
|
17
|
+
dataRouter.use(errorHandler);
|
|
17
18
|
/**
|
|
18
19
|
* @openapi
|
|
19
|
-
* /
|
|
20
|
+
* /session/start:
|
|
20
21
|
* post:
|
|
21
22
|
* tags:
|
|
22
23
|
* - Session
|
|
@@ -44,7 +45,7 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
44
45
|
* 500:
|
|
45
46
|
* description: Internal server error
|
|
46
47
|
*/
|
|
47
|
-
|
|
48
|
+
dataRouter.post("/session/start", [
|
|
48
49
|
generalRateLimit,
|
|
49
50
|
body("code")
|
|
50
51
|
.isString()
|
|
@@ -52,13 +53,27 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
52
53
|
.isLength({ min: 1, max: 50 })
|
|
53
54
|
.withMessage("Code must be between 1 and 50 characters"),
|
|
54
55
|
handleValidationErrors,
|
|
55
|
-
|
|
56
|
+
requireAuthType("student"),
|
|
56
57
|
], async (req, res) => {
|
|
57
58
|
try {
|
|
58
59
|
const { code } = req.body;
|
|
59
|
-
const db = getDatabase(
|
|
60
|
-
const sessionInfo = await
|
|
60
|
+
const db = getDatabase();
|
|
61
|
+
const sessionInfo = await getSession(code.toUpperCase(), db, req.user.uid);
|
|
62
|
+
// we need to store the mapping between userId and session code.
|
|
63
|
+
// so it is stored in the database and cache
|
|
61
64
|
if (sessionInfo) {
|
|
65
|
+
const uppercaseCode = code.toUpperCase();
|
|
66
|
+
await cache.set(req.user.uid, {
|
|
67
|
+
code: uppercaseCode,
|
|
68
|
+
teacherId: sessionInfo?.teacherId,
|
|
69
|
+
deliveryId: sessionInfo?.deliveryId,
|
|
70
|
+
});
|
|
71
|
+
const fs = getFirestore();
|
|
72
|
+
await fs.doc(db.AUTH.DOC(req.user.uid)).set({
|
|
73
|
+
code: uppercaseCode,
|
|
74
|
+
teacherId: sessionInfo?.teacherId,
|
|
75
|
+
deliveryId: sessionInfo?.deliveryId,
|
|
76
|
+
});
|
|
62
77
|
return sendSuccess(res, { ...sessionInfo, userId: req.user.uid });
|
|
63
78
|
}
|
|
64
79
|
else {
|
|
@@ -72,7 +87,7 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
72
87
|
});
|
|
73
88
|
/**
|
|
74
89
|
* @openapi
|
|
75
|
-
* /
|
|
90
|
+
* /delivery/start:
|
|
76
91
|
* post:
|
|
77
92
|
* tags:
|
|
78
93
|
* - Session
|
|
@@ -106,13 +121,13 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
106
121
|
* 500:
|
|
107
122
|
* description: Internal server error
|
|
108
123
|
*/
|
|
109
|
-
|
|
124
|
+
dataRouter.post("/delivery/session/start", [
|
|
110
125
|
generalRateLimit,
|
|
111
|
-
body("
|
|
126
|
+
body("code")
|
|
112
127
|
.isString()
|
|
113
128
|
.trim()
|
|
114
129
|
.isLength({ min: 1, max: 50 })
|
|
115
|
-
.withMessage("
|
|
130
|
+
.withMessage("Delivery code must be between 1 and 50 characters"),
|
|
116
131
|
body("studentIdentification")
|
|
117
132
|
.optional()
|
|
118
133
|
.isString()
|
|
@@ -120,14 +135,26 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
120
135
|
.isLength({ max: 100 })
|
|
121
136
|
.withMessage("Student identification must not exceed 100 characters"),
|
|
122
137
|
handleValidationErrors,
|
|
123
|
-
|
|
138
|
+
requireAuthType("student"),
|
|
124
139
|
], async (req, res) => {
|
|
125
140
|
try {
|
|
126
|
-
const {
|
|
127
|
-
const db = getDatabase(
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
const { code, studentIdentification } = req.body;
|
|
142
|
+
const db = getDatabase();
|
|
143
|
+
const delivery = await getDelivery(code.toUpperCase(), db);
|
|
144
|
+
// create a session based on the delivery.
|
|
145
|
+
// Create a unique code for the session to be able to restart the session and give it an unique id
|
|
146
|
+
if (!delivery) {
|
|
147
|
+
return sendError(res, 404, "Delivery not found");
|
|
148
|
+
}
|
|
149
|
+
const session = await createSessionForDelivery(db, req.user.uid, delivery);
|
|
150
|
+
const uppercaseCode = code.toUpperCase();
|
|
151
|
+
await cache.set(req.user.uid, uppercaseCode);
|
|
152
|
+
const fs = getFirestore();
|
|
153
|
+
await fs.doc(db.AUTH.DOC(req.user.uid)).set({
|
|
154
|
+
code: uppercaseCode,
|
|
155
|
+
});
|
|
156
|
+
if (session) {
|
|
157
|
+
return sendSuccess(res, session);
|
|
131
158
|
}
|
|
132
159
|
else {
|
|
133
160
|
return sendSuccess(res, null);
|
|
@@ -139,70 +166,76 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
139
166
|
}
|
|
140
167
|
});
|
|
141
168
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
169
|
+
* @openapi
|
|
170
|
+
* /testsetSession/start:
|
|
171
|
+
* post:
|
|
172
|
+
* tags:
|
|
173
|
+
* - Session
|
|
174
|
+
* summary: Use to login by testset session code. Retrieves testset session with associated test sessions.
|
|
175
|
+
* description: Checks if the provided testset session code is valid and retrieves the testset session information along with all associated test sessions.
|
|
176
|
+
* requestBody:
|
|
177
|
+
* required: true
|
|
178
|
+
* content:
|
|
179
|
+
* application/json:
|
|
180
|
+
* schema:
|
|
181
|
+
* type: object
|
|
182
|
+
* required:
|
|
183
|
+
* - code
|
|
184
|
+
* properties:
|
|
185
|
+
* code:
|
|
186
|
+
* type: string
|
|
187
|
+
* description: The testset session code.
|
|
188
|
+
* minLength: 1
|
|
189
|
+
* maxLength: 50
|
|
190
|
+
* responses:
|
|
191
|
+
* 200:
|
|
192
|
+
* description: Successfully retrieved testset session information with associated test sessions or null if not found.
|
|
193
|
+
* 400:
|
|
194
|
+
* description: Validation error
|
|
195
|
+
* 401:
|
|
196
|
+
* description: Authentication failed
|
|
197
|
+
* 404:
|
|
198
|
+
* description: Testset session not found
|
|
199
|
+
* 500:
|
|
200
|
+
* description: Internal server error
|
|
144
201
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
imageUploadConfig.single("screenshot"),
|
|
149
|
-
body("type")
|
|
202
|
+
dataRouter.post("/testsetSession/start", [
|
|
203
|
+
generalRateLimit,
|
|
204
|
+
body("code")
|
|
150
205
|
.isString()
|
|
151
206
|
.trim()
|
|
152
207
|
.isLength({ min: 1, max: 50 })
|
|
153
|
-
.withMessage("
|
|
154
|
-
body("description")
|
|
155
|
-
.isString()
|
|
156
|
-
.trim()
|
|
157
|
-
.isLength({ min: 1, max: 2000 })
|
|
158
|
-
.withMessage("Description is required and must be between 1 and 2000 characters"),
|
|
159
|
-
body("feedbackId")
|
|
160
|
-
.isString()
|
|
161
|
-
.trim()
|
|
162
|
-
.isLength({ min: 1, max: 100 })
|
|
163
|
-
.withMessage("Feedback ID is required"),
|
|
164
|
-
body("email")
|
|
165
|
-
.optional()
|
|
166
|
-
.isEmail()
|
|
167
|
-
.withMessage("Must be a valid email address"),
|
|
168
|
-
body("pageUrl").optional().isURL().withMessage("Must be a valid URL"),
|
|
208
|
+
.withMessage("Testset session code must be between 1 and 50 characters"),
|
|
169
209
|
handleValidationErrors,
|
|
210
|
+
requireAuthType("student"),
|
|
170
211
|
], async (req, res) => {
|
|
171
212
|
try {
|
|
172
|
-
const {
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
//
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const feedbackRef = firestore.doc(firebaseDocPath);
|
|
180
|
-
await feedbackRef.set({
|
|
181
|
-
type,
|
|
182
|
-
description,
|
|
183
|
-
userId,
|
|
184
|
-
email: email || null,
|
|
185
|
-
pageUrl: pageUrl || null,
|
|
186
|
-
hasScreenshot: !!file,
|
|
187
|
-
createdAt: new Date().toISOString(),
|
|
188
|
-
});
|
|
189
|
-
// If there's a screenshot, upload it to Firebase Storage
|
|
190
|
-
if (file) {
|
|
191
|
-
const storage = getStorage();
|
|
192
|
-
const bucket = storage.bucket();
|
|
193
|
-
const storagePath = firebaseDocPath.replace(/^\//, "");
|
|
194
|
-
const storageFile = bucket.file(`${storagePath}.png`);
|
|
195
|
-
await storageFile.save(file.buffer, {
|
|
196
|
-
metadata: {
|
|
197
|
-
contentType: file.mimetype,
|
|
198
|
-
},
|
|
199
|
-
});
|
|
213
|
+
const { code } = req.body;
|
|
214
|
+
const db = getDatabase();
|
|
215
|
+
const uppercaseCode = code.toUpperCase();
|
|
216
|
+
// Get the testset session with populated sessions
|
|
217
|
+
const result = await getTestsetSessionWithSessions(uppercaseCode, db, req.user.uid);
|
|
218
|
+
if (!result) {
|
|
219
|
+
return sendError(res, 404, "Testset session not found");
|
|
200
220
|
}
|
|
201
|
-
|
|
221
|
+
// Store the mapping between userId and testset session code
|
|
222
|
+
await cache.set(req.user.uid, {
|
|
223
|
+
code: uppercaseCode,
|
|
224
|
+
teacherId: result?.teacherId,
|
|
225
|
+
testsetId: result?.testsetId,
|
|
226
|
+
startableCodes: result?.sessionIds || [],
|
|
227
|
+
});
|
|
228
|
+
const fs = getFirestore();
|
|
229
|
+
await fs.doc(db.AUTH.DOC(req.user.uid)).set({
|
|
230
|
+
code: uppercaseCode,
|
|
231
|
+
teacherId: result?.teacherId,
|
|
232
|
+
testsetId: result?.testsetId,
|
|
233
|
+
});
|
|
234
|
+
return sendSuccess(res, result);
|
|
202
235
|
}
|
|
203
236
|
catch (error) {
|
|
204
|
-
console.error("Error
|
|
205
|
-
return sendError(res, 500, "Failed to
|
|
237
|
+
console.error("Error in testsetSession start:", error);
|
|
238
|
+
return sendError(res, 500, "Failed to start testset session");
|
|
206
239
|
}
|
|
207
240
|
});
|
|
208
241
|
/**
|
|
@@ -240,7 +273,7 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
240
273
|
* 500:
|
|
241
274
|
* description: Internal server error
|
|
242
275
|
*/
|
|
243
|
-
|
|
276
|
+
dataRouter.post("/student/log", [
|
|
244
277
|
generalRateLimit,
|
|
245
278
|
body("type")
|
|
246
279
|
.isString()
|
|
@@ -249,23 +282,23 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
249
282
|
.withMessage("Type must be a string between 1-100 characters"),
|
|
250
283
|
body("data").exists().withMessage("Data field is required"),
|
|
251
284
|
handleValidationErrors,
|
|
252
|
-
|
|
285
|
+
requireAuthType("student"),
|
|
253
286
|
], async (req, res) => {
|
|
254
287
|
try {
|
|
255
288
|
const { type, data } = req.body;
|
|
256
|
-
const db = getDatabase(
|
|
257
|
-
|
|
258
|
-
|
|
289
|
+
const db = getDatabase();
|
|
290
|
+
const { code, teacherId, deliveryId } = await getSessionInfoFromLoggedInUser(req);
|
|
291
|
+
let logEntry = {
|
|
292
|
+
createdAt: new Date().getTime(),
|
|
293
|
+
createdBy: req.user.uid,
|
|
259
294
|
type,
|
|
260
|
-
data,
|
|
261
|
-
timestamp: new Date().toISOString(),
|
|
262
|
-
studentId: req.user.uid,
|
|
263
|
-
appId: req.appId,
|
|
295
|
+
data: data,
|
|
264
296
|
};
|
|
297
|
+
if (code) {
|
|
298
|
+
logEntry = { ...logEntry, code, teacherId, deliveryId };
|
|
299
|
+
}
|
|
265
300
|
const firestore = getFirestore();
|
|
266
|
-
await firestore
|
|
267
|
-
.doc(db.STUDENT.LOG.DOC(req.user.uid, dateId()))
|
|
268
|
-
.set(logEntry);
|
|
301
|
+
await firestore.doc(db.SESSION.LOG.DOC(code, dateId())).set(logEntry);
|
|
269
302
|
return sendSuccess(res, undefined, "Log entry created successfully");
|
|
270
303
|
}
|
|
271
304
|
catch (error) {
|
|
@@ -275,195 +308,166 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
275
308
|
});
|
|
276
309
|
/**
|
|
277
310
|
* @openapi
|
|
278
|
-
* /
|
|
311
|
+
* /student/log:
|
|
279
312
|
* post:
|
|
280
313
|
* tags:
|
|
281
|
-
* -
|
|
282
|
-
* summary:
|
|
283
|
-
* description:
|
|
314
|
+
* - Student
|
|
315
|
+
* summary: Log student activity
|
|
316
|
+
* description: Logs student activity data for debugging and analytics purposes.
|
|
284
317
|
* requestBody:
|
|
285
318
|
* required: true
|
|
286
319
|
* content:
|
|
287
320
|
* application/json:
|
|
288
321
|
* schema:
|
|
289
322
|
* type: object
|
|
323
|
+
* required:
|
|
324
|
+
* - type
|
|
325
|
+
* - data
|
|
290
326
|
* properties:
|
|
291
|
-
*
|
|
292
|
-
* type: string
|
|
293
|
-
* description: The assessment code.
|
|
294
|
-
* identification:
|
|
327
|
+
* type:
|
|
295
328
|
* type: string
|
|
296
|
-
*
|
|
329
|
+
* minLength: 1
|
|
330
|
+
* maxLength: 100
|
|
331
|
+
* description: The type/category of the log entry
|
|
332
|
+
* data:
|
|
333
|
+
* description: Any data to be logged
|
|
297
334
|
* responses:
|
|
298
335
|
* 200:
|
|
299
|
-
* description: Successfully
|
|
300
|
-
*
|
|
301
|
-
* description:
|
|
336
|
+
* description: Successfully logged the data.
|
|
337
|
+
* 400:
|
|
338
|
+
* description: Invalid request data
|
|
339
|
+
* 401:
|
|
340
|
+
* description: Unauthorized
|
|
302
341
|
* 500:
|
|
303
|
-
* description:
|
|
342
|
+
* description: Internal server error
|
|
304
343
|
*/
|
|
305
|
-
|
|
344
|
+
dataRouter.post("/student/log", [
|
|
306
345
|
generalRateLimit,
|
|
307
|
-
body("
|
|
308
|
-
.isString()
|
|
309
|
-
.trim()
|
|
310
|
-
.isLength({ min: 1, max: 50 })
|
|
311
|
-
.withMessage("Code is required"),
|
|
312
|
-
body("identification")
|
|
313
|
-
.optional()
|
|
346
|
+
body("type")
|
|
314
347
|
.isString()
|
|
315
348
|
.trim()
|
|
316
|
-
.isLength({ max: 100 })
|
|
349
|
+
.isLength({ min: 1, max: 100 })
|
|
350
|
+
.withMessage("Type must be a string between 1-100 characters"),
|
|
351
|
+
body("data").exists().withMessage("Data field is required"),
|
|
317
352
|
handleValidationErrors,
|
|
318
|
-
|
|
353
|
+
requireAuthType("student"),
|
|
319
354
|
], async (req, res) => {
|
|
320
355
|
try {
|
|
321
|
-
const {
|
|
322
|
-
const db = getDatabase(
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
{
|
|
334
|
-
assessmentId: assessmentInfo.assessmentId,
|
|
335
|
-
assessmentName: assessmentInfo.name,
|
|
336
|
-
sessionState: "not_started",
|
|
337
|
-
packageId: assessmentInfo.packageId,
|
|
338
|
-
},
|
|
339
|
-
],
|
|
340
|
-
teacherId: assessmentInfo.teacherId || "general",
|
|
341
|
-
isDemo: true,
|
|
342
|
-
};
|
|
343
|
-
return sendSuccess(res, demoInfo);
|
|
344
|
-
}
|
|
345
|
-
const studentInfo = await specificImplementation.createSessionInfoForStudent({
|
|
346
|
-
userId: req.user.uid,
|
|
347
|
-
assessmentId: assessmentInfo.assessmentId,
|
|
348
|
-
identification,
|
|
349
|
-
metadata: {
|
|
350
|
-
isDemo: assessmentInfo.isDemo,
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
if (!studentInfo) {
|
|
354
|
-
return sendError(res, 500, "Error creating session");
|
|
355
|
-
}
|
|
356
|
-
return sendSuccess(res, studentInfo);
|
|
356
|
+
const { type, data } = req.body;
|
|
357
|
+
const db = getDatabase();
|
|
358
|
+
const { code, teacherId, deliveryId } = await getSessionInfoFromLoggedInUser(req);
|
|
359
|
+
const logEntry = {
|
|
360
|
+
createdAt: new Date().getTime(),
|
|
361
|
+
createdBy: req.user.uid,
|
|
362
|
+
type,
|
|
363
|
+
data: data && code ? { ...data, code, teacherId, deliveryId } : data,
|
|
364
|
+
};
|
|
365
|
+
const firestore = getFirestore();
|
|
366
|
+
await firestore.doc(db.SESSION.LOG.DOC(code, dateId())).set(logEntry);
|
|
367
|
+
return sendSuccess(res, undefined, "Log entry created successfully");
|
|
357
368
|
}
|
|
358
369
|
catch (error) {
|
|
359
|
-
console.error("Error
|
|
360
|
-
return sendError(res, 500, "Failed to
|
|
370
|
+
console.error("Error creating log entry:", error);
|
|
371
|
+
return sendError(res, 500, "Failed to create log entry");
|
|
361
372
|
}
|
|
362
373
|
});
|
|
363
374
|
/**
|
|
364
375
|
* @openapi
|
|
365
|
-
* /session/
|
|
366
|
-
*
|
|
376
|
+
* /session/info:
|
|
377
|
+
* get:
|
|
367
378
|
* tags:
|
|
368
379
|
* - Session
|
|
369
|
-
* summary:
|
|
370
|
-
* description:
|
|
380
|
+
* summary: Get session info
|
|
381
|
+
* description: Retrieves session information for the authenticated student.
|
|
371
382
|
*/
|
|
372
|
-
|
|
373
|
-
generalRateLimit,
|
|
374
|
-
body("assessmentId")
|
|
375
|
-
.isString()
|
|
376
|
-
.trim()
|
|
377
|
-
.isLength({ min: 1 })
|
|
378
|
-
.withMessage("Assessment ID is required"),
|
|
379
|
-
body("identification").optional().isString().trim(),
|
|
380
|
-
body("metadata").optional().isObject(),
|
|
381
|
-
handleValidationErrors,
|
|
382
|
-
requireAuth,
|
|
383
|
-
], async (req, res) => {
|
|
383
|
+
dataRouter.get("/session/info", [generalRateLimit, requireAuth], async (req, res) => {
|
|
384
384
|
try {
|
|
385
|
-
const {
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (!sessionInfo && canStart) {
|
|
396
|
-
sessionInfo =
|
|
397
|
-
await specificImplementation.createSessionInfoForStudent({
|
|
398
|
-
userId: req.user.uid,
|
|
399
|
-
identification,
|
|
400
|
-
assessmentId: assessmentId,
|
|
401
|
-
metadata: validMetadata,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
if ((sessionInfo && canStart) || sessionInfo?.isDemo) {
|
|
405
|
-
return sendSuccess(res, { ...sessionInfo, userId: req.user.uid });
|
|
385
|
+
const { code } = await getSessionInfoFromLoggedInUser(req);
|
|
386
|
+
const fs = getFirestore();
|
|
387
|
+
const db = getDatabase();
|
|
388
|
+
const sessionRef = fs.doc(db.SESSION.DOC(code));
|
|
389
|
+
const sessionSnapshot = await sessionRef.get();
|
|
390
|
+
const session = sessionSnapshot.exists
|
|
391
|
+
? sessionSnapshot.data()
|
|
392
|
+
: null;
|
|
393
|
+
if (session) {
|
|
394
|
+
return sendSuccess(res, session);
|
|
406
395
|
}
|
|
407
396
|
else {
|
|
408
|
-
return
|
|
397
|
+
return sendError(res, 404, "Session not found");
|
|
409
398
|
}
|
|
410
399
|
}
|
|
411
400
|
catch (error) {
|
|
412
|
-
console.error("Error
|
|
413
|
-
return sendError(res, 500, "Failed to
|
|
401
|
+
console.error("Error getting session info:", error);
|
|
402
|
+
return sendError(res, 500, "Failed to get session info");
|
|
414
403
|
}
|
|
415
404
|
});
|
|
416
405
|
/**
|
|
417
406
|
* @openapi
|
|
418
|
-
* /
|
|
407
|
+
* /testsetSession/info:
|
|
419
408
|
* get:
|
|
420
409
|
* tags:
|
|
421
410
|
* - Session
|
|
422
|
-
* summary: Get session info
|
|
423
|
-
* description: Retrieves session information for the authenticated student.
|
|
411
|
+
* summary: Get testset session info
|
|
412
|
+
* description: Retrieves testset session information with associated test sessions for the authenticated student.
|
|
413
|
+
* responses:
|
|
414
|
+
* 200:
|
|
415
|
+
* description: Successfully retrieved testset session information with associated test sessions.
|
|
416
|
+
* 401:
|
|
417
|
+
* description: Authentication failed
|
|
418
|
+
* 404:
|
|
419
|
+
* description: Testset session not found
|
|
420
|
+
* 500:
|
|
421
|
+
* description: Internal server error
|
|
424
422
|
*/
|
|
425
|
-
|
|
423
|
+
dataRouter.get("/testsetSession/info", [generalRateLimit, requireAuth], async (req, res) => {
|
|
426
424
|
try {
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return sendSuccess(res, sessionInfo);
|
|
425
|
+
const db = getDatabase();
|
|
426
|
+
const { code: uppercaseCode } = await getSessionInfoFromLoggedInUser(req);
|
|
427
|
+
if (!uppercaseCode) {
|
|
428
|
+
return sendError(res, 404, "No active session found for user");
|
|
432
429
|
}
|
|
433
|
-
|
|
434
|
-
|
|
430
|
+
// Get the testset session with populated sessions
|
|
431
|
+
const result = await getTestsetSessionWithSessions(uppercaseCode, db, req.user.uid);
|
|
432
|
+
if (!result) {
|
|
433
|
+
return sendError(res, 404, "Testset session not found");
|
|
435
434
|
}
|
|
435
|
+
return sendSuccess(res, result);
|
|
436
436
|
}
|
|
437
437
|
catch (error) {
|
|
438
|
-
console.error("Error getting session info:", error);
|
|
439
|
-
return sendError(res, 500, "Failed to get session info");
|
|
438
|
+
console.error("Error getting testset session info:", error);
|
|
439
|
+
return sendError(res, 500, "Failed to get testset session info");
|
|
440
440
|
}
|
|
441
441
|
});
|
|
442
442
|
/**
|
|
443
443
|
* @openapi
|
|
444
|
-
* /session/{
|
|
444
|
+
* /session/{code}/context:
|
|
445
445
|
* get:
|
|
446
446
|
* tags:
|
|
447
447
|
* - Session
|
|
448
448
|
* summary: Get session context
|
|
449
|
-
* description: Retrieves session context for a specific
|
|
449
|
+
* description: Retrieves session context for a specific session.
|
|
450
450
|
*/
|
|
451
|
-
|
|
451
|
+
dataRouter.get("/session/:code/context", [
|
|
452
452
|
generalRateLimit,
|
|
453
|
-
param("
|
|
453
|
+
param("code")
|
|
454
454
|
.isString()
|
|
455
455
|
.trim()
|
|
456
456
|
.isLength({ min: 1 })
|
|
457
|
-
.withMessage("
|
|
457
|
+
.withMessage("Session code is required"),
|
|
458
458
|
handleValidationErrors,
|
|
459
|
-
|
|
459
|
+
requireAuthType("student"),
|
|
460
460
|
], async (req, res) => {
|
|
461
461
|
try {
|
|
462
|
-
const {
|
|
463
|
-
const db = getDatabase(
|
|
464
|
-
const
|
|
462
|
+
const { code } = req.params;
|
|
463
|
+
const db = getDatabase();
|
|
464
|
+
const uppercaseCode = code.toUpperCase();
|
|
465
|
+
const { startableCodes } = await getSessionInfoFromLoggedInUser(req);
|
|
466
|
+
if (!startableCodes.includes(uppercaseCode)) {
|
|
467
|
+
return sendError(res, 403, "Code mismatch");
|
|
468
|
+
}
|
|
465
469
|
const firestore = getFirestore();
|
|
466
|
-
const sessionRef = firestore.doc(db.
|
|
470
|
+
const sessionRef = firestore.doc(db.SESSION.TEST_CONTEXT(code));
|
|
467
471
|
const context = await sessionRef.get();
|
|
468
472
|
return sendSuccess(res, context.exists ? context.data() : null);
|
|
469
473
|
}
|
|
@@ -474,34 +478,38 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
474
478
|
});
|
|
475
479
|
/**
|
|
476
480
|
* @openapi
|
|
477
|
-
* /session/{
|
|
481
|
+
* /session/{code}/context:
|
|
478
482
|
* post:
|
|
479
483
|
* tags:
|
|
480
484
|
* - Session
|
|
481
485
|
* summary: Update session context
|
|
482
486
|
* description: Updates session context for a specific assessment.
|
|
483
487
|
*/
|
|
484
|
-
|
|
488
|
+
dataRouter.post("/session/:code/context", [
|
|
485
489
|
generalRateLimit,
|
|
486
|
-
param("
|
|
490
|
+
param("code")
|
|
487
491
|
.isString()
|
|
488
492
|
.trim()
|
|
489
493
|
.isLength({ min: 1 })
|
|
490
|
-
.withMessage("
|
|
494
|
+
.withMessage("code is required"),
|
|
491
495
|
body("state").optional().isString(),
|
|
492
496
|
body("items").optional().isArray(),
|
|
493
497
|
handleValidationErrors,
|
|
494
|
-
|
|
498
|
+
requireAuthType("student"),
|
|
495
499
|
], async (req, res) => {
|
|
496
500
|
try {
|
|
497
|
-
const {
|
|
501
|
+
const { code: routeCode } = req.params;
|
|
498
502
|
const testContext = req.body;
|
|
499
|
-
const db = getDatabase(
|
|
500
|
-
const code =
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
+
const db = getDatabase();
|
|
504
|
+
const code = routeCode.toUpperCase();
|
|
505
|
+
const { code: codeRetrievedFromUser, teacherId, deliveryId, startableCodes, } = await getSessionInfoFromLoggedInUser(req);
|
|
506
|
+
if (!startableCodes.includes(code)) {
|
|
507
|
+
return sendError(res, 403, "Code mismatch");
|
|
508
|
+
}
|
|
509
|
+
const sessionKey = `test-context-${code}`;
|
|
510
|
+
let previousState = cache.get(sessionKey);
|
|
503
511
|
const firestore = getFirestore();
|
|
504
|
-
const sessionRef = firestore.doc(db.
|
|
512
|
+
const sessionRef = firestore.doc(db.SESSION.TEST_CONTEXT(code));
|
|
505
513
|
if (!previousState) {
|
|
506
514
|
const previousContext = await sessionRef.get();
|
|
507
515
|
if (previousContext.exists) {
|
|
@@ -511,14 +519,22 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
511
519
|
}
|
|
512
520
|
const batch = firestore.batch();
|
|
513
521
|
if (previousState !== testContext.state) {
|
|
514
|
-
cache.set(
|
|
515
|
-
const teacherId = await
|
|
522
|
+
cache.set(sessionKey, testContext.state, 60 * 60 * 1000); // 1 hour
|
|
523
|
+
const teacherId = await getTeacherIdBySessionCode(code);
|
|
516
524
|
if (!teacherId) {
|
|
517
525
|
return sendError(res, 404, "Teacher not found for this code");
|
|
518
526
|
}
|
|
527
|
+
// Get session to extract assessmentId
|
|
528
|
+
const sessionDocRef = firestore.doc(db.SESSION.DOC(code));
|
|
529
|
+
const sessionDoc = await sessionDocRef.get();
|
|
530
|
+
const session = sessionDoc.data();
|
|
531
|
+
const assessmentId = session?.assessmentId;
|
|
532
|
+
if (!assessmentId) {
|
|
533
|
+
return sendError(res, 404, "Assessment ID not found for this session");
|
|
534
|
+
}
|
|
519
535
|
await Promise.all([
|
|
520
|
-
specificImplementation.testContextToTeacher(code, assessmentId, testContext, batch),
|
|
521
|
-
|
|
536
|
+
specificImplementation.testContextToTeacher(code, assessmentId, deliveryId, teacherId, testContext, batch),
|
|
537
|
+
updateSessionState(db, code, testContext?.state ?? "not_started", batch),
|
|
522
538
|
specificImplementation.updateItemStats(teacherId, assessmentId, code, testContext.items, "teacher", batch),
|
|
523
539
|
]);
|
|
524
540
|
}
|
|
@@ -531,53 +547,235 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
531
547
|
return sendError(res, 500, "Failed to update session context");
|
|
532
548
|
}
|
|
533
549
|
});
|
|
550
|
+
const getSessionInfoFromLoggedInUser = async (req) => {
|
|
551
|
+
if (cache.get(req.user.uid)) {
|
|
552
|
+
return cache.get(req.user.uid);
|
|
553
|
+
}
|
|
554
|
+
const db = getDatabase();
|
|
555
|
+
const fs = getFirestore();
|
|
556
|
+
const sessionRef = fs.doc(db.AUTH.DOC(req.user.uid));
|
|
557
|
+
const data = await sessionRef.get().then((doc) => doc.data());
|
|
558
|
+
const { code, teacherId, deliveryId, testsetId } = data || {};
|
|
559
|
+
// Build list of startable codes
|
|
560
|
+
const startableCodes = [];
|
|
561
|
+
// If we have a code from sessionRef, add it to startable codes
|
|
562
|
+
if (code) {
|
|
563
|
+
startableCodes.push(code);
|
|
564
|
+
}
|
|
565
|
+
// If no session found in sessionRef, check testsetsessions using testsetId
|
|
566
|
+
if (testsetId) {
|
|
567
|
+
try {
|
|
568
|
+
const testsetSession = await getTestsetSessions(code, db);
|
|
569
|
+
if (testsetSession && testsetSession.sessionIds) {
|
|
570
|
+
startableCodes.push(...testsetSession.sessionIds);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
console.error("Error fetching testset sessions:", error);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const result = { code, teacherId, deliveryId, testsetId, startableCodes };
|
|
578
|
+
cache.set(req.user.uid, result);
|
|
579
|
+
return result;
|
|
580
|
+
};
|
|
581
|
+
const getTeacherIdBySessionCode = async (code) => {
|
|
582
|
+
if (cache.get(code)) {
|
|
583
|
+
return cache.get(code);
|
|
584
|
+
}
|
|
585
|
+
const db = getDatabase();
|
|
586
|
+
const fs = getFirestore();
|
|
587
|
+
const sessionRef = fs.doc(db.SESSION.DOC(code));
|
|
588
|
+
const sessionDoc = await sessionRef.get();
|
|
589
|
+
const teacherId = sessionDoc.data()?.teacherId;
|
|
590
|
+
cache.set(code, teacherId);
|
|
591
|
+
return teacherId;
|
|
592
|
+
};
|
|
534
593
|
/**
|
|
535
594
|
* @openapi
|
|
536
|
-
* /session/{
|
|
537
|
-
*
|
|
595
|
+
* /session/{code}/update:
|
|
596
|
+
* put:
|
|
538
597
|
* tags:
|
|
539
598
|
* - Session
|
|
540
|
-
* summary: Update session
|
|
541
|
-
* description: Updates
|
|
599
|
+
* summary: Update student session information
|
|
600
|
+
* description: Updates specific fields of a student session.
|
|
601
|
+
* parameters:
|
|
602
|
+
* - in: path
|
|
603
|
+
* name: code
|
|
604
|
+
* required: true
|
|
605
|
+
* schema:
|
|
606
|
+
* type: string
|
|
607
|
+
* description: The session code
|
|
608
|
+
* requestBody:
|
|
609
|
+
* required: true
|
|
610
|
+
* content:
|
|
611
|
+
* application/json:
|
|
612
|
+
* schema:
|
|
613
|
+
* type: object
|
|
614
|
+
* description: Partial session data to update
|
|
615
|
+
* responses:
|
|
616
|
+
* 200:
|
|
617
|
+
* description: Successfully updated session information.
|
|
618
|
+
* 403:
|
|
619
|
+
* description: Code mismatch
|
|
620
|
+
* 500:
|
|
621
|
+
* description: Internal server error
|
|
542
622
|
*/
|
|
543
|
-
|
|
623
|
+
dataRouter.put("/session/:code/update", [
|
|
544
624
|
generalRateLimit,
|
|
545
|
-
param("
|
|
625
|
+
param("code")
|
|
546
626
|
.isString()
|
|
547
627
|
.trim()
|
|
548
628
|
.isLength({ min: 1 })
|
|
549
|
-
.withMessage("
|
|
550
|
-
|
|
629
|
+
.withMessage("Session code is required"),
|
|
630
|
+
handleValidationErrors,
|
|
631
|
+
requireAuthType("student"),
|
|
632
|
+
], async (req, res) => {
|
|
633
|
+
try {
|
|
634
|
+
const { code } = req.params;
|
|
635
|
+
const data = req.body;
|
|
636
|
+
const db = getDatabase();
|
|
637
|
+
const uppercaseCode = code.toUpperCase();
|
|
638
|
+
const { code: codeRetrievedFromUser, startableCodes } = await getSessionInfoFromLoggedInUser(req);
|
|
639
|
+
if (!startableCodes.includes(uppercaseCode)) {
|
|
640
|
+
return sendError(res, 403, "Code mismatch");
|
|
641
|
+
}
|
|
642
|
+
const firestore = getFirestore();
|
|
643
|
+
const sessionRef = firestore.doc(db.SESSION.DOC(uppercaseCode));
|
|
644
|
+
await sessionRef.update(data);
|
|
645
|
+
return sendSuccess(res, undefined, "Session updated successfully");
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
console.error("Error updating session:", error);
|
|
649
|
+
return sendError(res, 500, "Failed to update session");
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
/**
|
|
653
|
+
* @openapi
|
|
654
|
+
* /assessment/code/{code}:
|
|
655
|
+
* get:
|
|
656
|
+
* tags:
|
|
657
|
+
* - Assessment
|
|
658
|
+
* summary: Get assessment by code
|
|
659
|
+
* description: Retrieves assessment information by code.
|
|
660
|
+
* parameters:
|
|
661
|
+
* - in: path
|
|
662
|
+
* name: code
|
|
663
|
+
* required: true
|
|
664
|
+
* schema:
|
|
665
|
+
* type: string
|
|
666
|
+
* description: The assessment code
|
|
667
|
+
* responses:
|
|
668
|
+
* 200:
|
|
669
|
+
* description: Successfully retrieved assessment information.
|
|
670
|
+
* 404:
|
|
671
|
+
* description: Assessment not found
|
|
672
|
+
* 500:
|
|
673
|
+
* description: Internal server error
|
|
674
|
+
*/
|
|
675
|
+
dataRouter.get("/assessment/code/:code", [
|
|
676
|
+
generalRateLimit,
|
|
677
|
+
param("code")
|
|
551
678
|
.isString()
|
|
552
679
|
.trim()
|
|
553
680
|
.isLength({ min: 1 })
|
|
554
|
-
.withMessage("
|
|
681
|
+
.withMessage("Assessment code is required"),
|
|
555
682
|
handleValidationErrors,
|
|
556
|
-
|
|
683
|
+
requireAuthType("student"),
|
|
557
684
|
], async (req, res) => {
|
|
558
685
|
try {
|
|
559
|
-
const {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
686
|
+
const { code } = req.params;
|
|
687
|
+
const db = getDatabase();
|
|
688
|
+
// This would need to be implemented based on your database structure
|
|
689
|
+
// For now, returning a placeholder implementation
|
|
690
|
+
return sendError(res, 501, "Assessment by code endpoint not yet implemented");
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
console.error("Error getting assessment by code:", error);
|
|
694
|
+
return sendError(res, 500, "Failed to get assessment");
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
/**
|
|
698
|
+
* @openapi
|
|
699
|
+
* /currentUser:
|
|
700
|
+
* get:
|
|
701
|
+
* tags:
|
|
702
|
+
* - User
|
|
703
|
+
* summary: Get current user information
|
|
704
|
+
* description: Retrieves information about the currently authenticated user.
|
|
705
|
+
* responses:
|
|
706
|
+
* 200:
|
|
707
|
+
* description: Successfully retrieved user information.
|
|
708
|
+
* 401:
|
|
709
|
+
* description: Unauthorized
|
|
710
|
+
* 500:
|
|
711
|
+
* description: Internal server error
|
|
712
|
+
*/
|
|
713
|
+
dataRouter.get("/currentUser", [generalRateLimit, requireAuth], async (req, res) => {
|
|
714
|
+
try {
|
|
715
|
+
// Return basic user info from the authenticated request
|
|
716
|
+
const userInfo = {
|
|
717
|
+
userId: req.user.uid,
|
|
718
|
+
// Add other user properties as needed
|
|
719
|
+
};
|
|
720
|
+
return sendSuccess(res, userInfo);
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
console.error("Error getting current user:", error);
|
|
724
|
+
return sendError(res, 500, "Failed to get current user");
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
/**
|
|
728
|
+
* @openapi
|
|
729
|
+
* /session/{code}/score:
|
|
730
|
+
* post:
|
|
731
|
+
* tags:
|
|
732
|
+
* - Session
|
|
733
|
+
* summary: Score items in a session
|
|
734
|
+
* description: Scores all items in a session and returns the updated test context.
|
|
735
|
+
* parameters:
|
|
736
|
+
* - in: path
|
|
737
|
+
* name: code
|
|
738
|
+
* required: true
|
|
739
|
+
* schema:
|
|
740
|
+
* type: string
|
|
741
|
+
* description: The session code
|
|
742
|
+
* responses:
|
|
743
|
+
* 200:
|
|
744
|
+
* description: Successfully scored items and returned test context.
|
|
745
|
+
* 403:
|
|
746
|
+
* description: Code mismatch
|
|
747
|
+
* 500:
|
|
748
|
+
* description: Internal server error
|
|
749
|
+
*/
|
|
750
|
+
dataRouter.post("/session/:code/score", [
|
|
751
|
+
generalRateLimit,
|
|
752
|
+
param("code")
|
|
753
|
+
.isString()
|
|
754
|
+
.trim()
|
|
755
|
+
.isLength({ min: 1 })
|
|
756
|
+
.withMessage("Session code is required"),
|
|
757
|
+
handleValidationErrors,
|
|
758
|
+
requireAuthType("student"),
|
|
759
|
+
], async (req, res) => {
|
|
760
|
+
try {
|
|
761
|
+
const { code } = req.params;
|
|
762
|
+
const db = getDatabase();
|
|
763
|
+
const uppercaseCode = code.toUpperCase();
|
|
764
|
+
const { code: codeRetrievedFromUser, startableCodes } = await getSessionInfoFromLoggedInUser(req);
|
|
765
|
+
if (!startableCodes.includes(uppercaseCode)) {
|
|
766
|
+
return sendError(res, 403, "Code mismatch");
|
|
574
767
|
}
|
|
575
|
-
|
|
576
|
-
|
|
768
|
+
// This would need to implement the scoring logic
|
|
769
|
+
// For now, returning a placeholder implementation
|
|
770
|
+
const firestore = getFirestore();
|
|
771
|
+
const sessionRef = firestore.doc(db.SESSION.DOC(uppercaseCode));
|
|
772
|
+
const sessionDoc = await sessionRef.get();
|
|
773
|
+
const testContext = sessionDoc.data();
|
|
774
|
+
return sendSuccess(res, testContext);
|
|
577
775
|
}
|
|
578
776
|
catch (error) {
|
|
579
|
-
console.error("Error
|
|
580
|
-
return sendError(res, 500, "Failed to
|
|
777
|
+
console.error("Error scoring items:", error);
|
|
778
|
+
return sendError(res, 500, "Failed to score items");
|
|
581
779
|
}
|
|
582
780
|
});
|
|
583
781
|
/**
|
|
@@ -586,37 +784,187 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
|
|
|
586
784
|
* post:
|
|
587
785
|
* tags:
|
|
588
786
|
* - Session
|
|
589
|
-
* summary: Log session
|
|
590
|
-
* description: Logs
|
|
787
|
+
* summary: Log action for a session
|
|
788
|
+
* description: Logs an action with payload for a specific session.
|
|
789
|
+
* parameters:
|
|
790
|
+
* - in: path
|
|
791
|
+
* name: assessmentId
|
|
792
|
+
* required: true
|
|
793
|
+
* schema:
|
|
794
|
+
* type: string
|
|
795
|
+
* description: The session/assessment identifier
|
|
796
|
+
* requestBody:
|
|
797
|
+
* required: true
|
|
798
|
+
* content:
|
|
799
|
+
* application/json:
|
|
800
|
+
* schema:
|
|
801
|
+
* type: object
|
|
802
|
+
* properties:
|
|
803
|
+
* type:
|
|
804
|
+
* type: string
|
|
805
|
+
* description: The action type
|
|
806
|
+
* payload:
|
|
807
|
+
* description: The action payload
|
|
808
|
+
* time:
|
|
809
|
+
* type: string
|
|
810
|
+
* description: The timestamp
|
|
811
|
+
* createdBy:
|
|
812
|
+
* type: string
|
|
813
|
+
* description: User ID who created the log
|
|
814
|
+
* responses:
|
|
815
|
+
* 200:
|
|
816
|
+
* description: Successfully logged the action.
|
|
817
|
+
* 500:
|
|
818
|
+
* description: Internal server error
|
|
591
819
|
*/
|
|
592
|
-
|
|
820
|
+
dataRouter.post("/session/:assessmentId/log", [
|
|
593
821
|
generalRateLimit,
|
|
594
822
|
param("assessmentId")
|
|
595
823
|
.isString()
|
|
596
824
|
.trim()
|
|
597
825
|
.isLength({ min: 1 })
|
|
598
826
|
.withMessage("Assessment ID is required"),
|
|
599
|
-
body("
|
|
827
|
+
body("type")
|
|
828
|
+
.isString()
|
|
829
|
+
.trim()
|
|
830
|
+
.isLength({ min: 1 })
|
|
831
|
+
.withMessage("Type is required"),
|
|
600
832
|
handleValidationErrors,
|
|
601
|
-
|
|
833
|
+
requireAuthType("student"),
|
|
602
834
|
], async (req, res) => {
|
|
603
835
|
try {
|
|
604
836
|
const { assessmentId } = req.params;
|
|
605
|
-
const
|
|
606
|
-
const
|
|
837
|
+
const { type, payload, time, createdBy } = req.body;
|
|
838
|
+
const db = getDatabase();
|
|
839
|
+
const logEntry = {
|
|
840
|
+
type,
|
|
841
|
+
payload,
|
|
842
|
+
time: time || dateId(),
|
|
843
|
+
createdBy: createdBy || req.user.uid,
|
|
844
|
+
createdAt: new Date().getTime(),
|
|
845
|
+
};
|
|
607
846
|
const firestore = getFirestore();
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
847
|
+
await firestore
|
|
848
|
+
.doc(db.SESSION.LOG.DOC(assessmentId, logEntry.time))
|
|
849
|
+
.set(logEntry);
|
|
850
|
+
return sendSuccess(res, undefined, "Action logged successfully");
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
console.error("Error logging action:", error);
|
|
854
|
+
return sendError(res, 500, "Failed to log action");
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
/**
|
|
858
|
+
* @openapi
|
|
859
|
+
* /checkCode:
|
|
860
|
+
* post:
|
|
861
|
+
* tags:
|
|
862
|
+
* - Session
|
|
863
|
+
* summary: Check if a code is valid and get session info
|
|
864
|
+
* description: Checks if the provided code is valid and retrieves session information.
|
|
865
|
+
* requestBody:
|
|
866
|
+
* required: true
|
|
867
|
+
* content:
|
|
868
|
+
* application/json:
|
|
869
|
+
* schema:
|
|
870
|
+
* type: object
|
|
871
|
+
* properties:
|
|
872
|
+
* code:
|
|
873
|
+
* type: string
|
|
874
|
+
* description: The session code.
|
|
875
|
+
* responses:
|
|
876
|
+
* 200:
|
|
877
|
+
* description: Successfully retrieved session information or null if not found.
|
|
878
|
+
* 500:
|
|
879
|
+
* description: Internal server error
|
|
880
|
+
*/
|
|
881
|
+
dataRouter.post("/checkCode", [
|
|
882
|
+
generalRateLimit,
|
|
883
|
+
body("code")
|
|
884
|
+
.isString()
|
|
885
|
+
.trim()
|
|
886
|
+
.isLength({ min: 1, max: 50 })
|
|
887
|
+
.withMessage("Code must be between 1 and 50 characters"),
|
|
888
|
+
handleValidationErrors,
|
|
889
|
+
requireAuthType("student"),
|
|
890
|
+
], async (req, res) => {
|
|
891
|
+
try {
|
|
892
|
+
const { code } = req.body;
|
|
893
|
+
const db = getDatabase();
|
|
894
|
+
const sessionInfo = await getSession(code.toUpperCase(), db, req.user.uid);
|
|
895
|
+
return sendSuccess(res, sessionInfo);
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
console.error("Error in checkCode:", error);
|
|
899
|
+
return sendError(res, 500, "Failed to check code");
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
/**
|
|
903
|
+
* @openapi
|
|
904
|
+
* /feedback:
|
|
905
|
+
* post:
|
|
906
|
+
* tags:
|
|
907
|
+
* - Student
|
|
908
|
+
* summary: Submit student feedback
|
|
909
|
+
* description: Endpoint for students to submit feedback with optional screenshot
|
|
910
|
+
* requestBody:
|
|
911
|
+
* required: true
|
|
912
|
+
* content:
|
|
913
|
+
* multipart/form-data:
|
|
914
|
+
* schema:
|
|
915
|
+
* type: object
|
|
916
|
+
* required:
|
|
917
|
+
* - type
|
|
918
|
+
* - description
|
|
919
|
+
* - feedbackId
|
|
920
|
+
* properties:
|
|
921
|
+
* type:
|
|
922
|
+
* type: string
|
|
923
|
+
* description: Type of feedback
|
|
924
|
+
* description:
|
|
925
|
+
* type: string
|
|
926
|
+
* description: Feedback description
|
|
927
|
+
* feedbackId:
|
|
928
|
+
* type: string
|
|
929
|
+
* description: Unique feedback identifier
|
|
930
|
+
* email:
|
|
931
|
+
* type: string
|
|
932
|
+
* description: User email (optional)
|
|
933
|
+
* pageUrl:
|
|
934
|
+
* type: string
|
|
935
|
+
* description: URL of the page where feedback was submitted
|
|
936
|
+
* screenshot:
|
|
937
|
+
* type: string
|
|
938
|
+
* format: binary
|
|
939
|
+
* description: Optional screenshot file
|
|
940
|
+
* responses:
|
|
941
|
+
* 200:
|
|
942
|
+
* description: Feedback submitted successfully
|
|
943
|
+
* 400:
|
|
944
|
+
* description: Missing required fields or validation error
|
|
945
|
+
* 401:
|
|
946
|
+
* description: Authentication required
|
|
947
|
+
* 500:
|
|
948
|
+
* description: Server error
|
|
949
|
+
*/
|
|
950
|
+
dataRouter.post("/feedback", [generalRateLimit, requireAuthType("student")], async (req, res) => {
|
|
951
|
+
try {
|
|
952
|
+
const userId = req.user.uid;
|
|
953
|
+
const { data, fileBuffer } = await processFeedbackSubmission(req);
|
|
954
|
+
// Validate the feedback data
|
|
955
|
+
const validationError = validateFeedbackData(data);
|
|
956
|
+
if (validationError) {
|
|
957
|
+
return sendError(res, 400, validationError);
|
|
958
|
+
}
|
|
959
|
+
// Save feedback with student user type
|
|
960
|
+
await saveFeedback(data, userId, 'student', fileBuffer);
|
|
961
|
+
return sendSuccess(res, undefined, "Feedback submitted successfully");
|
|
615
962
|
}
|
|
616
963
|
catch (error) {
|
|
617
|
-
console.error("Error
|
|
618
|
-
return sendError(res, 500, "
|
|
964
|
+
console.error("Error submitting student feedback:", error);
|
|
965
|
+
return sendError(res, 500, "An unexpected error occurred");
|
|
619
966
|
}
|
|
620
967
|
});
|
|
968
|
+
app.use("/", dataRouter);
|
|
621
969
|
}
|
|
622
970
|
//# sourceMappingURL=qti-data.js.map
|