@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-teacher.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { authenticateTeacher } from "../helpers/request-headers";
|
|
3
|
+
import { createDelivery, deleteDelivery, getAssessment, getAssessments, reopenSession, resetSession, processFeedbackSubmission, validateFeedbackData, saveFeedback, } from "./../helpers/logic";
|
|
3
4
|
import { getDatabase } from "../helpers/db";
|
|
4
5
|
import { getFirestore } from "../helpers/firebase";
|
|
5
|
-
import { body, param
|
|
6
|
+
import { body, param } from "express-validator";
|
|
6
7
|
import { generalRateLimit, handleValidationErrors, heavyOperationLimit, sendError, sendSuccess, validateAssessmentId, } from "../helpers/endpoint-helpers";
|
|
7
|
-
import { dateId, createCode } from "../helpers";
|
|
8
8
|
import { BaseImplementation } from "./app-specific/baseImplementation";
|
|
9
|
+
import { DeliveryStateEnum } from "@citolab/qti-api";
|
|
10
|
+
import { sort } from "../helpers";
|
|
9
11
|
// Teacher authentication middleware
|
|
10
12
|
export const requireTeacherAuth = async (req, res, next) => {
|
|
11
13
|
try {
|
|
@@ -14,7 +16,6 @@ export const requireTeacherAuth = async (req, res, next) => {
|
|
|
14
16
|
return sendError(res, 401, "Teacher authentication required");
|
|
15
17
|
}
|
|
16
18
|
req.user = result.user;
|
|
17
|
-
req.appId = getAppId(req);
|
|
18
19
|
next();
|
|
19
20
|
}
|
|
20
21
|
catch (error) {
|
|
@@ -22,32 +23,6 @@ export const requireTeacherAuth = async (req, res, next) => {
|
|
|
22
23
|
return sendError(res, 500, "Authentication failed");
|
|
23
24
|
}
|
|
24
25
|
};
|
|
25
|
-
const checkCode = async (db, groupCode) => {
|
|
26
|
-
try {
|
|
27
|
-
const firestore = getFirestore();
|
|
28
|
-
const doc = await firestore.doc(db.GROUP_DELIVERY.DOC(groupCode)).get();
|
|
29
|
-
return doc.exists;
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
console.error("Error checking code:", error);
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const createDelivery = async (db) => {
|
|
37
|
-
try {
|
|
38
|
-
let groupCode = createCode(4);
|
|
39
|
-
let exists = await checkCode(db, groupCode);
|
|
40
|
-
while (exists || groupCode.toLowerCase() === "aaaa") {
|
|
41
|
-
groupCode = createCode(4);
|
|
42
|
-
exists = await checkCode(db, groupCode);
|
|
43
|
-
}
|
|
44
|
-
return groupCode;
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
console.error("Error creating activity:", error);
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
26
|
// Validation helpers
|
|
52
27
|
const validateCode = (code) => {
|
|
53
28
|
return (typeof code === "string" &&
|
|
@@ -55,32 +30,11 @@ const validateCode = (code) => {
|
|
|
55
30
|
code.length <= 50 &&
|
|
56
31
|
/^[a-zA-Z0-9_-]+$/.test(code));
|
|
57
32
|
};
|
|
58
|
-
const validateTarget = (target) => {
|
|
59
|
-
return target === "teacher" || target === "reviewer";
|
|
60
|
-
};
|
|
61
|
-
const parseScore = (scoreValue) => {
|
|
62
|
-
if (scoreValue === null || scoreValue === undefined || scoreValue === "") {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
if (typeof scoreValue === "number") {
|
|
66
|
-
return isFinite(scoreValue) ? scoreValue : null;
|
|
67
|
-
}
|
|
68
|
-
if (typeof scoreValue === "string") {
|
|
69
|
-
// Valid integer (positive or negative)
|
|
70
|
-
if (/^-?\d+$/.test(scoreValue)) {
|
|
71
|
-
return parseInt(scoreValue, 10);
|
|
72
|
-
}
|
|
73
|
-
// Valid float
|
|
74
|
-
if (/^-?\d*\.\d+$/.test(scoreValue)) {
|
|
75
|
-
return parseFloat(scoreValue);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
};
|
|
80
33
|
export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImplementation()) {
|
|
34
|
+
const teacherRouter = Router();
|
|
81
35
|
/**
|
|
82
36
|
* @openapi
|
|
83
|
-
* /
|
|
37
|
+
* /delivery/create:
|
|
84
38
|
* post:
|
|
85
39
|
* tags:
|
|
86
40
|
* - Teacher
|
|
@@ -117,124 +71,128 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
|
|
|
117
71
|
* 500:
|
|
118
72
|
* description: Internal server error
|
|
119
73
|
*/
|
|
120
|
-
|
|
74
|
+
teacherRouter.post("/delivery/create", [
|
|
121
75
|
heavyOperationLimit,
|
|
122
76
|
body("assessmentId").notEmpty().withMessage("Assessment ID is required"),
|
|
123
77
|
handleValidationErrors,
|
|
124
78
|
requireTeacherAuth,
|
|
125
79
|
], async (req, res) => {
|
|
126
80
|
try {
|
|
81
|
+
const user = await authenticateTeacher(req, res);
|
|
82
|
+
const userId = user.user?.uid || "";
|
|
127
83
|
const { assessmentId } = req.body;
|
|
128
|
-
const db = getDatabase(
|
|
129
|
-
const code = await createDelivery(db);
|
|
84
|
+
const db = getDatabase();
|
|
85
|
+
const code = await createDelivery(db, userId, assessmentId);
|
|
130
86
|
if (!code) {
|
|
131
87
|
return sendError(res, 500, "Failed to create activity code");
|
|
132
88
|
}
|
|
133
89
|
const firestore = getFirestore();
|
|
134
|
-
|
|
135
|
-
await firestore.doc(db.GROUP_DELIVERY.DOC(code)).set({
|
|
136
|
-
groupCode: code,
|
|
90
|
+
const delivery = {
|
|
137
91
|
assessmentId,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
92
|
+
createdAt: new Date().getTime(),
|
|
93
|
+
updatedAt: new Date().getTime(),
|
|
94
|
+
createdBy: userId,
|
|
95
|
+
id: code,
|
|
96
|
+
state: DeliveryStateEnum.NOT_STARTED,
|
|
97
|
+
};
|
|
98
|
+
// Create the activity document
|
|
99
|
+
await firestore.doc(db.DELIVERY.DOC(code)).set(delivery);
|
|
100
|
+
return sendSuccess(res, delivery, "Group delivery created successfully");
|
|
143
101
|
}
|
|
144
102
|
catch (error) {
|
|
145
103
|
console.error("Error creating group delivery:", error);
|
|
146
104
|
return sendError(res, 500, "Failed to create group delivery");
|
|
147
105
|
}
|
|
148
106
|
});
|
|
107
|
+
// delete delivery
|
|
149
108
|
/**
|
|
150
109
|
* @openapi
|
|
151
|
-
* /
|
|
152
|
-
*
|
|
110
|
+
* /delivery/delete:
|
|
111
|
+
* post:
|
|
153
112
|
* tags:
|
|
154
113
|
* - Teacher
|
|
155
|
-
* summary:
|
|
114
|
+
* summary: Delete a delivery
|
|
115
|
+
* requestBody:
|
|
116
|
+
* required: true
|
|
117
|
+
* content:
|
|
118
|
+
* application/json:
|
|
119
|
+
* schema:
|
|
120
|
+
* type: object
|
|
121
|
+
* required:
|
|
122
|
+
* - code
|
|
123
|
+
* properties:
|
|
124
|
+
* code:
|
|
125
|
+
* type: string
|
|
126
|
+
* description: The delivery code to delete
|
|
156
127
|
* responses:
|
|
157
128
|
* 200:
|
|
158
|
-
* description:
|
|
129
|
+
* description: Delivery deleted successfully
|
|
130
|
+
* 400:
|
|
131
|
+
* description: Invalid request data
|
|
159
132
|
* 401:
|
|
160
133
|
* description: Unauthorized
|
|
161
134
|
* 404:
|
|
162
|
-
* description:
|
|
135
|
+
* description: Delivery not found
|
|
163
136
|
* 500:
|
|
164
137
|
* description: Internal server error
|
|
165
138
|
*/
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return sendError(res, 404, "Application not found");
|
|
139
|
+
teacherRouter.post("/delivery/delete", [
|
|
140
|
+
generalRateLimit,
|
|
141
|
+
body("code").custom((value) => {
|
|
142
|
+
if (!validateCode(value)) {
|
|
143
|
+
throw new Error("Invalid code format");
|
|
172
144
|
}
|
|
173
|
-
return
|
|
145
|
+
return true;
|
|
146
|
+
}),
|
|
147
|
+
handleValidationErrors,
|
|
148
|
+
requireTeacherAuth,
|
|
149
|
+
], async (req, res) => {
|
|
150
|
+
try {
|
|
151
|
+
const { code } = req.body;
|
|
152
|
+
const db = getDatabase();
|
|
153
|
+
await deleteDelivery(db, code);
|
|
154
|
+
return sendSuccess(res, { code }, "Delivery deleted successfully");
|
|
174
155
|
}
|
|
175
156
|
catch (error) {
|
|
176
|
-
console.error("Error
|
|
177
|
-
return sendError(res, 500, "Failed to
|
|
157
|
+
console.error("Error deleting delivery:", error);
|
|
158
|
+
return sendError(res, 500, "Failed to delete delivery");
|
|
178
159
|
}
|
|
179
160
|
});
|
|
180
161
|
/**
|
|
181
162
|
* @openapi
|
|
182
|
-
* /
|
|
163
|
+
* /assessments:
|
|
183
164
|
* get:
|
|
184
165
|
* tags:
|
|
185
166
|
* - Teacher
|
|
186
|
-
* summary: Get
|
|
167
|
+
* summary: Get all assessments
|
|
187
168
|
* responses:
|
|
188
169
|
* 200:
|
|
189
|
-
* description: Returns
|
|
170
|
+
* description: Returns all assessments
|
|
190
171
|
* 401:
|
|
191
172
|
* description: Unauthorized
|
|
173
|
+
* 404:
|
|
174
|
+
* description: Application not found
|
|
192
175
|
* 500:
|
|
193
176
|
* description: Internal server error
|
|
194
177
|
*/
|
|
195
|
-
|
|
178
|
+
teacherRouter.get("/assessments", [generalRateLimit, requireTeacherAuth], async (req, res) => {
|
|
196
179
|
try {
|
|
197
|
-
const db = getDatabase(
|
|
198
|
-
const
|
|
199
|
-
return sendSuccess(res,
|
|
180
|
+
const db = getDatabase();
|
|
181
|
+
const assessments = await getAssessments(db);
|
|
182
|
+
return sendSuccess(res, { assessments });
|
|
200
183
|
}
|
|
201
184
|
catch (error) {
|
|
202
|
-
console.error("Error retrieving
|
|
203
|
-
return sendError(res, 500, "Failed to retrieve
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
app.get("/teacher/assessment/:assessmentId/itemStats", [
|
|
207
|
-
generalRateLimit,
|
|
208
|
-
param("assessmentId").custom((value) => {
|
|
209
|
-
if (!validateAssessmentId(value)) {
|
|
210
|
-
throw new Error("Invalid assessment ID format");
|
|
211
|
-
}
|
|
212
|
-
return true;
|
|
213
|
-
}),
|
|
214
|
-
query("role").optional().isIn(["teacher", "reviewer"]),
|
|
215
|
-
handleValidationErrors,
|
|
216
|
-
requireTeacherAuth, // You might want to make this more generic
|
|
217
|
-
], async (req, res) => {
|
|
218
|
-
try {
|
|
219
|
-
const { assessmentId } = req.params;
|
|
220
|
-
const role = req.query.role || "teacher"; // default to teacher
|
|
221
|
-
const db = getDatabase(req.appId);
|
|
222
|
-
const studentSessionInfos = await getItemsStats(db, req.user.uid, assessmentId, role);
|
|
223
|
-
return sendSuccess(res, studentSessionInfos);
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
console.error("Error retrieving item stats:", error);
|
|
227
|
-
return sendError(res, 500, "Failed to retrieve item statistics");
|
|
185
|
+
console.error("Error retrieving assessments:", error);
|
|
186
|
+
return sendError(res, 500, "Failed to retrieve assessments");
|
|
228
187
|
}
|
|
229
188
|
});
|
|
230
189
|
/**
|
|
231
190
|
* @openapi
|
|
232
|
-
* /
|
|
191
|
+
* /delivery/stop:
|
|
233
192
|
* post:
|
|
234
193
|
* tags:
|
|
235
194
|
* - Teacher
|
|
236
|
-
* summary:
|
|
237
|
-
* description: Logs teacher activity data for debugging and analytics purposes.
|
|
195
|
+
* summary: Stop a delivery
|
|
238
196
|
* requestBody:
|
|
239
197
|
* required: true
|
|
240
198
|
* content:
|
|
@@ -242,109 +200,92 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
|
|
|
242
200
|
* schema:
|
|
243
201
|
* type: object
|
|
244
202
|
* required:
|
|
245
|
-
* -
|
|
246
|
-
* - data
|
|
203
|
+
* - code
|
|
247
204
|
* properties:
|
|
248
|
-
*
|
|
205
|
+
* code:
|
|
249
206
|
* type: string
|
|
250
|
-
*
|
|
251
|
-
* maxLength: 100
|
|
252
|
-
* description: The type/category of the log entry
|
|
253
|
-
* data:
|
|
254
|
-
* description: Any data to be logged
|
|
207
|
+
* description: The delivery code to stop
|
|
255
208
|
* responses:
|
|
256
209
|
* 200:
|
|
257
|
-
* description:
|
|
210
|
+
* description: Delivery stopped successfully
|
|
258
211
|
* 400:
|
|
259
212
|
* description: Invalid request data
|
|
260
213
|
* 401:
|
|
261
214
|
* description: Unauthorized
|
|
215
|
+
* 404:
|
|
216
|
+
* description: Delivery not found
|
|
262
217
|
* 500:
|
|
263
218
|
* description: Internal server error
|
|
264
219
|
*/
|
|
265
|
-
|
|
220
|
+
teacherRouter.post("/delivery/stop", [
|
|
266
221
|
generalRateLimit,
|
|
267
|
-
body("
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
222
|
+
body("code").custom((value) => {
|
|
223
|
+
if (!validateCode(value)) {
|
|
224
|
+
throw new Error("Invalid code format");
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}),
|
|
273
228
|
handleValidationErrors,
|
|
274
229
|
requireTeacherAuth,
|
|
275
230
|
], async (req, res) => {
|
|
276
231
|
try {
|
|
277
|
-
const {
|
|
278
|
-
const db = getDatabase(
|
|
279
|
-
// Create log entry with timestamp and teacher ID
|
|
280
|
-
const logEntry = {
|
|
281
|
-
type,
|
|
282
|
-
data,
|
|
283
|
-
timestamp: new Date().toISOString(),
|
|
284
|
-
teacherId: req.user.uid,
|
|
285
|
-
appId: req.appId,
|
|
286
|
-
};
|
|
232
|
+
const { code } = req.body;
|
|
233
|
+
const db = getDatabase();
|
|
287
234
|
const firestore = getFirestore();
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
235
|
+
const deliveryRef = firestore.doc(db.DELIVERY.DOC(code));
|
|
236
|
+
const delivery = await deliveryRef.get();
|
|
237
|
+
if (!delivery.exists) {
|
|
238
|
+
return sendError(res, 404, "Delivery not found");
|
|
239
|
+
}
|
|
240
|
+
const finishedAt = new Date().getTime();
|
|
241
|
+
await deliveryRef.update({
|
|
242
|
+
modifiedAt: finishedAt,
|
|
243
|
+
finishedAt,
|
|
244
|
+
state: "inactive",
|
|
245
|
+
});
|
|
246
|
+
return sendSuccess(res, { code, finishedAt }, "Delivery stopped successfully");
|
|
292
247
|
}
|
|
293
248
|
catch (error) {
|
|
294
|
-
console.error("Error
|
|
295
|
-
return sendError(res, 500, "Failed to
|
|
249
|
+
console.error("Error stopping delivery:", error);
|
|
250
|
+
return sendError(res, 500, "Failed to stop delivery");
|
|
296
251
|
}
|
|
297
252
|
});
|
|
298
253
|
/**
|
|
299
254
|
* @openapi
|
|
300
|
-
* /
|
|
255
|
+
* /delivery/start:
|
|
301
256
|
* post:
|
|
302
257
|
* tags:
|
|
303
258
|
* - Teacher
|
|
304
|
-
* summary:
|
|
259
|
+
* summary: Restart a delivery
|
|
305
260
|
* requestBody:
|
|
306
261
|
* required: true
|
|
307
262
|
* content:
|
|
308
263
|
* application/json:
|
|
309
264
|
* schema:
|
|
310
265
|
* type: object
|
|
266
|
+
* required:
|
|
267
|
+
* - code
|
|
311
268
|
* properties:
|
|
312
|
-
*
|
|
313
|
-
* type:
|
|
314
|
-
*
|
|
315
|
-
* maximum: 100
|
|
316
|
-
* assessmentIds:
|
|
317
|
-
* type: array
|
|
318
|
-
* items:
|
|
319
|
-
* type: string
|
|
320
|
-
* maxItems: 20
|
|
269
|
+
* code:
|
|
270
|
+
* type: string
|
|
271
|
+
* description: The delivery code to restart
|
|
321
272
|
* responses:
|
|
322
273
|
* 200:
|
|
323
|
-
* description:
|
|
274
|
+
* description: Delivery restarted successfully
|
|
324
275
|
* 400:
|
|
325
|
-
* description: Invalid request data
|
|
276
|
+
* description: Invalid request data or delivery cannot be restarted
|
|
326
277
|
* 401:
|
|
327
278
|
* description: Unauthorized
|
|
328
279
|
* 404:
|
|
329
|
-
* description:
|
|
280
|
+
* description: Delivery not found
|
|
330
281
|
* 500:
|
|
331
282
|
* description: Internal server error
|
|
332
283
|
*/
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
body("
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
.withMessage("Count must be between 1 and 100"),
|
|
339
|
-
body("assessmentIds")
|
|
340
|
-
.optional()
|
|
341
|
-
.isArray({ max: 20 })
|
|
342
|
-
.withMessage("Assessment IDs must be an array with max 20 items"),
|
|
343
|
-
body("assessmentIds.*")
|
|
344
|
-
.optional()
|
|
345
|
-
.custom((value) => {
|
|
346
|
-
if (!validateAssessmentId(value)) {
|
|
347
|
-
throw new Error("Invalid assessment ID format");
|
|
284
|
+
teacherRouter.post("/delivery/start", [
|
|
285
|
+
generalRateLimit,
|
|
286
|
+
body("code").custom((value) => {
|
|
287
|
+
if (!validateCode(value)) {
|
|
288
|
+
throw new Error("Invalid code format");
|
|
348
289
|
}
|
|
349
290
|
return true;
|
|
350
291
|
}),
|
|
@@ -352,82 +293,51 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
|
|
|
352
293
|
requireTeacherAuth,
|
|
353
294
|
], async (req, res) => {
|
|
354
295
|
try {
|
|
355
|
-
const {
|
|
356
|
-
const db = getDatabase(
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
296
|
+
const { code } = req.body;
|
|
297
|
+
const db = getDatabase();
|
|
298
|
+
const firestore = getFirestore();
|
|
299
|
+
const deliveryRef = firestore.doc(db.DELIVERY.DOC(code));
|
|
300
|
+
const delivery = await deliveryRef.get();
|
|
301
|
+
if (!delivery.exists) {
|
|
302
|
+
return sendError(res, 404, "Delivery not found");
|
|
360
303
|
}
|
|
361
|
-
|
|
362
|
-
|
|
304
|
+
await deliveryRef.update({
|
|
305
|
+
modifiedAt: new Date().getTime(),
|
|
306
|
+
finishedAt: null,
|
|
307
|
+
state: "active",
|
|
363
308
|
});
|
|
364
|
-
|
|
365
|
-
return sendError(res, 400, "No valid assessments found for the provided IDs");
|
|
366
|
-
}
|
|
367
|
-
const studentSessionInfos = await createSession(req.user.uid, db, count, assessmentsToPlan);
|
|
368
|
-
return sendSuccess(res, studentSessionInfos, "Sessions planned successfully");
|
|
309
|
+
return sendSuccess(res, { code }, "Delivery restarted successfully");
|
|
369
310
|
}
|
|
370
311
|
catch (error) {
|
|
371
|
-
console.error("Error
|
|
372
|
-
return sendError(res, 500, "Failed to
|
|
312
|
+
console.error("Error restarting delivery:", error);
|
|
313
|
+
return sendError(res, 500, "Failed to restart delivery");
|
|
373
314
|
}
|
|
374
315
|
});
|
|
375
316
|
/**
|
|
376
317
|
* @openapi
|
|
377
|
-
* /
|
|
378
|
-
*
|
|
318
|
+
* /assessment/{assessmentId}/deliveries:
|
|
319
|
+
* get:
|
|
379
320
|
* tags:
|
|
380
321
|
* - Teacher
|
|
381
|
-
* summary:
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* - identifications
|
|
390
|
-
* properties:
|
|
391
|
-
* identifications:
|
|
392
|
-
* type: array
|
|
393
|
-
* items:
|
|
394
|
-
* type: string
|
|
395
|
-
* minItems: 1
|
|
396
|
-
* maxItems: 100
|
|
397
|
-
* assessmentIds:
|
|
398
|
-
* type: array
|
|
399
|
-
* items:
|
|
400
|
-
* type: string
|
|
401
|
-
* maxItems: 20
|
|
322
|
+
* summary: Get all deliveries for an assessment
|
|
323
|
+
* parameters:
|
|
324
|
+
* - in: path
|
|
325
|
+
* name: assessmentId
|
|
326
|
+
* required: true
|
|
327
|
+
* schema:
|
|
328
|
+
* type: string
|
|
329
|
+
* description: The assessment ID
|
|
402
330
|
* responses:
|
|
403
331
|
* 200:
|
|
404
|
-
* description: Returns
|
|
405
|
-
* 400:
|
|
406
|
-
* description: Invalid request data
|
|
332
|
+
* description: Returns all deliveries for the assessment
|
|
407
333
|
* 401:
|
|
408
334
|
* description: Unauthorized
|
|
409
|
-
* 404:
|
|
410
|
-
* description: Application not found
|
|
411
335
|
* 500:
|
|
412
336
|
* description: Internal server error
|
|
413
337
|
*/
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
.isArray({ min: 1, max: 100 })
|
|
418
|
-
.withMessage("Identifications must be an array with 1-100 items"),
|
|
419
|
-
body("identifications.*")
|
|
420
|
-
.isString()
|
|
421
|
-
.trim()
|
|
422
|
-
.isLength({ min: 1, max: 100 })
|
|
423
|
-
.withMessage("Each identification must be 1-100 characters"),
|
|
424
|
-
body("assessmentIds")
|
|
425
|
-
.optional()
|
|
426
|
-
.isArray({ max: 20 })
|
|
427
|
-
.withMessage("Assessment IDs must be an array with max 20 items"),
|
|
428
|
-
body("assessmentIds.*")
|
|
429
|
-
.optional()
|
|
430
|
-
.custom((value) => {
|
|
338
|
+
teacherRouter.get("/assessment/:assessmentId/deliveries", [
|
|
339
|
+
generalRateLimit,
|
|
340
|
+
param("assessmentId").custom((value) => {
|
|
431
341
|
if (!validateAssessmentId(value)) {
|
|
432
342
|
throw new Error("Invalid assessment ID format");
|
|
433
343
|
}
|
|
@@ -437,97 +347,126 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
|
|
|
437
347
|
requireTeacherAuth,
|
|
438
348
|
], async (req, res) => {
|
|
439
349
|
try {
|
|
440
|
-
const {
|
|
441
|
-
const db = getDatabase(
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
350
|
+
const { assessmentId } = req.params;
|
|
351
|
+
const db = getDatabase();
|
|
352
|
+
const firestore = getFirestore();
|
|
353
|
+
const deliveriesQuery = firestore
|
|
354
|
+
.collection(db.DELIVERY.COLLECTION())
|
|
355
|
+
.where("assessmentId", "==", assessmentId)
|
|
356
|
+
.where("createdBy", "==", req.user.uid);
|
|
357
|
+
const deliveriesSnapshot = await deliveriesQuery.get();
|
|
358
|
+
const deliveries = deliveriesSnapshot.docs.map((doc) => {
|
|
359
|
+
return doc.data();
|
|
448
360
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
const studentSessionInfos = await createSessionByIdentifications(req.user.uid, db, identifications.map((i) => ({ identifier: i })), "not_started", assessmentsToPlan);
|
|
453
|
-
return sendSuccess(res, studentSessionInfos, "Sessions planned by identification successfully");
|
|
361
|
+
const sortedDeliveries = sort(deliveries, (d) => d.createdAt, true);
|
|
362
|
+
return sendSuccess(res, sortedDeliveries);
|
|
454
363
|
}
|
|
455
364
|
catch (error) {
|
|
456
|
-
console.error("Error
|
|
457
|
-
return sendError(res, 500, "Failed to
|
|
365
|
+
console.error("Error retrieving deliveries:", error);
|
|
366
|
+
return sendError(res, 500, "Failed to retrieve deliveries");
|
|
458
367
|
}
|
|
459
368
|
});
|
|
460
369
|
/**
|
|
461
370
|
* @openapi
|
|
462
|
-
* /
|
|
463
|
-
*
|
|
371
|
+
* /delivery/{assessmentId}/csv:
|
|
372
|
+
* get:
|
|
464
373
|
* tags:
|
|
465
374
|
* - Teacher
|
|
466
|
-
* summary:
|
|
375
|
+
* summary: Download results as CSV
|
|
467
376
|
* parameters:
|
|
468
377
|
* - in: path
|
|
469
|
-
* name:
|
|
378
|
+
* name: assessmentId
|
|
470
379
|
* required: true
|
|
471
380
|
* schema:
|
|
472
381
|
* type: string
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
382
|
+
* description: The assessment ID
|
|
383
|
+
* - in: query
|
|
384
|
+
* name: deliveryCode
|
|
385
|
+
* required: false
|
|
476
386
|
* schema:
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
* required: true
|
|
480
|
-
* content:
|
|
481
|
-
* application/json:
|
|
482
|
-
* schema:
|
|
483
|
-
* type: object
|
|
484
|
-
* required:
|
|
485
|
-
* - responseId
|
|
486
|
-
* properties:
|
|
487
|
-
* responseId:
|
|
488
|
-
* type: string
|
|
489
|
-
* scoreExternal:
|
|
490
|
-
* oneOf:
|
|
491
|
-
* - type: number
|
|
492
|
-
* - type: string
|
|
493
|
-
* - type: 'null'
|
|
494
|
-
* target:
|
|
495
|
-
* type: string
|
|
496
|
-
* enum: [teacher, reviewer]
|
|
387
|
+
* type: string
|
|
388
|
+
* description: Optional delivery code to filter results
|
|
497
389
|
* responses:
|
|
498
390
|
* 200:
|
|
499
|
-
* description:
|
|
500
|
-
*
|
|
501
|
-
*
|
|
391
|
+
* description: Returns CSV file with results
|
|
392
|
+
* content:
|
|
393
|
+
* text/csv:
|
|
394
|
+
* schema:
|
|
395
|
+
* type: string
|
|
396
|
+
* format: binary
|
|
502
397
|
* 401:
|
|
503
398
|
* description: Unauthorized
|
|
399
|
+
* 404:
|
|
400
|
+
* description: Assessment not found
|
|
504
401
|
* 500:
|
|
505
402
|
* description: Internal server error
|
|
506
403
|
*/
|
|
507
|
-
|
|
404
|
+
teacherRouter.get("/delivery/:code/csv", [
|
|
508
405
|
generalRateLimit,
|
|
509
|
-
param("
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
.isLength({ min: 1, max: 100 })
|
|
513
|
-
.matches(/^[a-zA-Z0-9_.-]+$/)
|
|
514
|
-
.withMessage("Item identifier must be alphanumeric with dots, dashes and underscores only"),
|
|
515
|
-
param("assessmentId").custom((value) => {
|
|
516
|
-
if (!validateAssessmentId(value)) {
|
|
517
|
-
throw new Error("Invalid assessment ID format");
|
|
406
|
+
param("code").custom((value) => {
|
|
407
|
+
if (!validateCode(value)) {
|
|
408
|
+
throw new Error("Invalid code format");
|
|
518
409
|
}
|
|
519
410
|
return true;
|
|
520
411
|
}),
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (
|
|
530
|
-
|
|
412
|
+
handleValidationErrors,
|
|
413
|
+
requireTeacherAuth,
|
|
414
|
+
], async (req, res) => {
|
|
415
|
+
try {
|
|
416
|
+
const { code } = req.params;
|
|
417
|
+
const db = getDatabase();
|
|
418
|
+
// Get assessment info
|
|
419
|
+
const assessment = await getAssessment(db, code);
|
|
420
|
+
if (!assessment) {
|
|
421
|
+
return sendError(res, 404, "Assessment not found");
|
|
422
|
+
}
|
|
423
|
+
// TODO: Implement actual results retrieval and CSV generation
|
|
424
|
+
// For now, return a placeholder CSV
|
|
425
|
+
const csvHeaders = "Assessment,DeliveryCode,StudentId,StudentName,StartTime,EndTime,Score,MaxScore,Percentage,Status\n";
|
|
426
|
+
const fileName = code
|
|
427
|
+
? `${assessment.name}_${code}_results.csv`
|
|
428
|
+
: `${assessment.name}_all_results.csv`;
|
|
429
|
+
const csvContent = code
|
|
430
|
+
? `${csvHeaders}${assessment.name},${code},,,,,,,No results yet\n`
|
|
431
|
+
: `${csvHeaders}${assessment.name},All,,,,,,,No results yet\n`;
|
|
432
|
+
res.setHeader("Content-Type", "text/csv");
|
|
433
|
+
res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
|
|
434
|
+
res.send(csvContent);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
console.error("Error downloading results:", error);
|
|
438
|
+
return sendError(res, 500, "Failed to download results");
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
/**
|
|
442
|
+
* @openapi
|
|
443
|
+
* /session/reset:
|
|
444
|
+
* post:
|
|
445
|
+
* tags:
|
|
446
|
+
* - Teacher
|
|
447
|
+
* summary: Reset session
|
|
448
|
+
* parameters:
|
|
449
|
+
* - in: query
|
|
450
|
+
* name: code
|
|
451
|
+
* required: true
|
|
452
|
+
* schema:
|
|
453
|
+
* type: string
|
|
454
|
+
* description: The session code
|
|
455
|
+
* responses:
|
|
456
|
+
* 200:
|
|
457
|
+
* description: Session reset successfully
|
|
458
|
+
* 401:
|
|
459
|
+
* description: Unauthorized
|
|
460
|
+
* 404:
|
|
461
|
+
* description: Session not found
|
|
462
|
+
* 500:
|
|
463
|
+
* description: Internal server error
|
|
464
|
+
*/
|
|
465
|
+
teacherRouter.post("/session/reset", [
|
|
466
|
+
generalRateLimit,
|
|
467
|
+
param("code").custom((value) => {
|
|
468
|
+
if (!validateCode(value)) {
|
|
469
|
+
throw new Error("Invalid code format");
|
|
531
470
|
}
|
|
532
471
|
return true;
|
|
533
472
|
}),
|
|
@@ -535,134 +474,128 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
|
|
|
535
474
|
requireTeacherAuth,
|
|
536
475
|
], async (req, res) => {
|
|
537
476
|
try {
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
const scoreExternal = parseScore(scoreRawValue);
|
|
544
|
-
const db = getDatabase(req.appId);
|
|
545
|
-
await updateItemStatResponseScore(db, req.user.uid, assessmentId, itemIdentifier, responseId, scoreExternal, target);
|
|
546
|
-
return sendSuccess(res, undefined, "Score updated successfully");
|
|
477
|
+
const code = req.params.code;
|
|
478
|
+
const userId = req.user.uid;
|
|
479
|
+
const db = getDatabase();
|
|
480
|
+
await resetSession(db, userId, code);
|
|
481
|
+
return sendSuccess(res, { message: "Session reset successfully" });
|
|
547
482
|
}
|
|
548
483
|
catch (error) {
|
|
549
|
-
console.error("Error
|
|
550
|
-
return sendError(res, 500, "Failed to
|
|
484
|
+
console.error("Error resetting session:", error);
|
|
485
|
+
return sendError(res, 500, "Failed to reset session");
|
|
551
486
|
}
|
|
552
487
|
});
|
|
553
488
|
/**
|
|
554
|
-
*
|
|
555
|
-
* /teacher/access:
|
|
489
|
+
* /session/reopen:
|
|
556
490
|
* post:
|
|
557
491
|
* tags:
|
|
558
492
|
* - Teacher
|
|
559
|
-
* summary:
|
|
560
|
-
*
|
|
493
|
+
* summary: Reopen session
|
|
494
|
+
* parameters:
|
|
495
|
+
* - in: query
|
|
496
|
+
* name: code
|
|
497
|
+
* required: true
|
|
498
|
+
* schema:
|
|
499
|
+
* type: string
|
|
500
|
+
* description: The session code
|
|
561
501
|
* responses:
|
|
562
502
|
* 200:
|
|
563
|
-
* description:
|
|
503
|
+
* description: Session reopened successfully
|
|
564
504
|
* 401:
|
|
565
505
|
* description: Unauthorized
|
|
506
|
+
* 404:
|
|
507
|
+
* description: Session not found
|
|
508
|
+
* 500:
|
|
509
|
+
* description: Internal server error
|
|
566
510
|
*/
|
|
567
|
-
|
|
568
|
-
|
|
511
|
+
teacherRouter.post("/session/reopen", [
|
|
512
|
+
generalRateLimit,
|
|
513
|
+
param("code").custom((value) => {
|
|
514
|
+
if (!validateCode(value)) {
|
|
515
|
+
throw new Error("Invalid code format");
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}),
|
|
519
|
+
handleValidationErrors,
|
|
520
|
+
requireTeacherAuth,
|
|
521
|
+
], async (req, res) => {
|
|
522
|
+
try {
|
|
523
|
+
const code = req.params.code;
|
|
524
|
+
const userId = req.user.uid;
|
|
525
|
+
const db = getDatabase();
|
|
526
|
+
await reopenSession(db, userId, code);
|
|
527
|
+
return sendSuccess(res, { message: "Session reopened successfully" });
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
console.error("Error resetting session:", error);
|
|
531
|
+
return sendError(res, 500, "Failed to reset session");
|
|
532
|
+
}
|
|
569
533
|
});
|
|
570
534
|
/**
|
|
571
535
|
* @openapi
|
|
572
|
-
* /
|
|
536
|
+
* /log:
|
|
573
537
|
* post:
|
|
574
538
|
* tags:
|
|
575
539
|
* - Teacher
|
|
576
|
-
* summary:
|
|
577
|
-
* description:
|
|
540
|
+
* summary: Log teacher activity
|
|
541
|
+
* description: Logs teacher activity data for debugging and analytics purposes.
|
|
578
542
|
* requestBody:
|
|
579
543
|
* required: true
|
|
580
544
|
* content:
|
|
581
545
|
* application/json:
|
|
582
546
|
* schema:
|
|
583
547
|
* type: object
|
|
584
|
-
* required:
|
|
585
|
-
* - assessmentId
|
|
586
|
-
* - code
|
|
587
|
-
* - session
|
|
588
548
|
* properties:
|
|
589
|
-
*
|
|
590
|
-
* type: string
|
|
591
|
-
* code:
|
|
549
|
+
* type:
|
|
592
550
|
* type: string
|
|
593
|
-
*
|
|
594
|
-
*
|
|
551
|
+
* description: The type/category of the log entry
|
|
552
|
+
* data:
|
|
553
|
+
* description: Any data to be logged
|
|
595
554
|
* responses:
|
|
596
555
|
* 200:
|
|
597
|
-
* description: Successfully
|
|
598
|
-
* 400:
|
|
599
|
-
* description: Invalid request data
|
|
600
|
-
* 401:
|
|
601
|
-
* description: Unauthorized
|
|
556
|
+
* description: Successfully logged the data.
|
|
602
557
|
* 500:
|
|
603
558
|
* description: Internal server error
|
|
604
559
|
*/
|
|
605
|
-
|
|
560
|
+
teacherRouter.post("/log", [
|
|
606
561
|
generalRateLimit,
|
|
607
|
-
body("
|
|
608
|
-
|
|
609
|
-
throw new Error("Invalid assessment ID format");
|
|
610
|
-
}
|
|
611
|
-
return true;
|
|
612
|
-
}),
|
|
613
|
-
body("code").custom((value) => {
|
|
614
|
-
if (!validateCode(value)) {
|
|
615
|
-
throw new Error("Invalid code format");
|
|
616
|
-
}
|
|
617
|
-
return true;
|
|
618
|
-
}),
|
|
619
|
-
body("session").isObject().withMessage("Session must be an object"),
|
|
562
|
+
body("type").isString().withMessage("Type must be a string"),
|
|
563
|
+
body("data").exists().withMessage("Data field is required"),
|
|
620
564
|
handleValidationErrors,
|
|
621
565
|
requireTeacherAuth,
|
|
622
566
|
], async (req, res) => {
|
|
623
567
|
try {
|
|
624
|
-
const {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const batch = firestore.batch();
|
|
628
|
-
await Promise.all([
|
|
629
|
-
specificImplementation.updatePlannedSession(req.user.uid, code, session, batch),
|
|
630
|
-
updateStudentSessionState(db, code, session.assessmentId, session.sessionState, batch),
|
|
631
|
-
]);
|
|
632
|
-
await batch.commit();
|
|
633
|
-
return sendSuccess(res, undefined, "Session updated successfully");
|
|
568
|
+
const { type, data } = req.body;
|
|
569
|
+
// Implementation would depend on your logging requirements
|
|
570
|
+
return sendSuccess(res, undefined, "Log entry created successfully");
|
|
634
571
|
}
|
|
635
572
|
catch (error) {
|
|
636
|
-
console.error("Error
|
|
637
|
-
return sendError(res, 500, "Failed to
|
|
573
|
+
console.error("Error creating log entry:", error);
|
|
574
|
+
return sendError(res, 500, "Failed to create log entry");
|
|
638
575
|
}
|
|
639
576
|
});
|
|
640
577
|
/**
|
|
641
578
|
* @openapi
|
|
642
|
-
* /
|
|
579
|
+
* /session/{code}:
|
|
643
580
|
* delete:
|
|
644
581
|
* tags:
|
|
645
582
|
* - Teacher
|
|
646
|
-
* summary: Delete
|
|
647
|
-
* description: Deletes a
|
|
583
|
+
* summary: Delete student session
|
|
584
|
+
* description: Deletes a student session.
|
|
648
585
|
* parameters:
|
|
649
586
|
* - in: path
|
|
650
587
|
* name: code
|
|
651
588
|
* required: true
|
|
652
589
|
* schema:
|
|
653
590
|
* type: string
|
|
654
|
-
* description: The session code
|
|
591
|
+
* description: The session code
|
|
655
592
|
* responses:
|
|
656
593
|
* 200:
|
|
657
|
-
* description: Successfully deleted
|
|
658
|
-
* 400:
|
|
659
|
-
* description: Invalid code format
|
|
660
|
-
* 401:
|
|
661
|
-
* description: Unauthorized
|
|
594
|
+
* description: Successfully deleted student session.
|
|
662
595
|
* 500:
|
|
663
596
|
* description: Internal server error
|
|
664
597
|
*/
|
|
665
|
-
|
|
598
|
+
teacherRouter.delete("/session/:code", [
|
|
666
599
|
generalRateLimit,
|
|
667
600
|
param("code").custom((value) => {
|
|
668
601
|
if (!validateCode(value)) {
|
|
@@ -675,48 +608,105 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
|
|
|
675
608
|
], async (req, res) => {
|
|
676
609
|
try {
|
|
677
610
|
const { code } = req.params;
|
|
678
|
-
const db = getDatabase(
|
|
679
|
-
|
|
680
|
-
|
|
611
|
+
const db = getDatabase();
|
|
612
|
+
const firestore = getFirestore();
|
|
613
|
+
// Delete the session document
|
|
614
|
+
await firestore.doc(db.SESSION.DOC(code.toUpperCase())).delete();
|
|
615
|
+
return sendSuccess(res, undefined, "Student session deleted successfully");
|
|
681
616
|
}
|
|
682
617
|
catch (error) {
|
|
683
|
-
console.error("Error deleting session:", error);
|
|
684
|
-
return sendError(res, 500, "Failed to delete session");
|
|
618
|
+
console.error("Error deleting student session:", error);
|
|
619
|
+
return sendError(res, 500, "Failed to delete student session");
|
|
685
620
|
}
|
|
686
621
|
});
|
|
687
622
|
/**
|
|
688
623
|
* @openapi
|
|
689
|
-
* /
|
|
624
|
+
* /student/update:
|
|
690
625
|
* post:
|
|
691
626
|
* tags:
|
|
692
627
|
* - Teacher
|
|
693
|
-
* summary:
|
|
694
|
-
* description:
|
|
628
|
+
* summary: Add student identification
|
|
629
|
+
* description: Updates student identification information.
|
|
630
|
+
* requestBody:
|
|
631
|
+
* required: true
|
|
632
|
+
* content:
|
|
633
|
+
* application/json:
|
|
634
|
+
* schema:
|
|
635
|
+
* type: object
|
|
636
|
+
* properties:
|
|
637
|
+
* code:
|
|
638
|
+
* type: string
|
|
639
|
+
* description: The session code
|
|
640
|
+
* identification:
|
|
641
|
+
* type: string
|
|
642
|
+
* description: The student identification
|
|
643
|
+
* responses:
|
|
644
|
+
* 200:
|
|
645
|
+
* description: Successfully updated student identification.
|
|
646
|
+
* 500:
|
|
647
|
+
* description: Internal server error
|
|
648
|
+
*/
|
|
649
|
+
teacherRouter.post("/student/update", [
|
|
650
|
+
generalRateLimit,
|
|
651
|
+
body("code").custom((value) => {
|
|
652
|
+
if (!validateCode(value)) {
|
|
653
|
+
throw new Error("Invalid code format");
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
}),
|
|
657
|
+
body("identification")
|
|
658
|
+
.isString()
|
|
659
|
+
.withMessage("Identification must be a string"),
|
|
660
|
+
handleValidationErrors,
|
|
661
|
+
requireTeacherAuth,
|
|
662
|
+
], async (req, res) => {
|
|
663
|
+
try {
|
|
664
|
+
const { code, identification } = req.body;
|
|
665
|
+
const db = getDatabase();
|
|
666
|
+
const firestore = getFirestore();
|
|
667
|
+
// Update the session with identification
|
|
668
|
+
await firestore.doc(db.SESSION.DOC(code.toUpperCase())).update({
|
|
669
|
+
identification,
|
|
670
|
+
updatedAt: new Date().getTime(),
|
|
671
|
+
});
|
|
672
|
+
return sendSuccess(res, undefined, "Student identification updated successfully");
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
console.error("Error updating student identification:", error);
|
|
676
|
+
return sendError(res, 500, "Failed to update student identification");
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
/**
|
|
680
|
+
* @openapi
|
|
681
|
+
* /session/update:
|
|
682
|
+
* post:
|
|
683
|
+
* tags:
|
|
684
|
+
* - Teacher
|
|
685
|
+
* summary: Update session
|
|
686
|
+
* description: Updates session information.
|
|
695
687
|
* requestBody:
|
|
696
688
|
* required: true
|
|
697
689
|
* content:
|
|
698
690
|
* application/json:
|
|
699
691
|
* schema:
|
|
700
692
|
* type: object
|
|
701
|
-
* required:
|
|
702
|
-
* - code
|
|
703
|
-
* - assessmentId
|
|
704
693
|
* properties:
|
|
705
694
|
* code:
|
|
706
695
|
* type: string
|
|
696
|
+
* description: The session code
|
|
707
697
|
* assessmentId:
|
|
708
698
|
* type: string
|
|
699
|
+
* description: The assessment ID
|
|
700
|
+
* session:
|
|
701
|
+
* type: object
|
|
702
|
+
* description: The session data to update
|
|
709
703
|
* responses:
|
|
710
704
|
* 200:
|
|
711
|
-
* description: Successfully
|
|
712
|
-
* 400:
|
|
713
|
-
* description: Invalid request data
|
|
714
|
-
* 401:
|
|
715
|
-
* description: Unauthorized
|
|
705
|
+
* description: Successfully updated session.
|
|
716
706
|
* 500:
|
|
717
707
|
* description: Internal server error
|
|
718
708
|
*/
|
|
719
|
-
|
|
709
|
+
teacherRouter.post("/session/update", [
|
|
720
710
|
generalRateLimit,
|
|
721
711
|
body("code").custom((value) => {
|
|
722
712
|
if (!validateCode(value)) {
|
|
@@ -730,78 +720,227 @@ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImp
|
|
|
730
720
|
}
|
|
731
721
|
return true;
|
|
732
722
|
}),
|
|
723
|
+
body("session").isObject().withMessage("Session must be an object"),
|
|
733
724
|
handleValidationErrors,
|
|
734
725
|
requireTeacherAuth,
|
|
735
726
|
], async (req, res) => {
|
|
736
727
|
try {
|
|
737
|
-
const { code, assessmentId } = req.body;
|
|
738
|
-
const db = getDatabase(
|
|
739
|
-
|
|
740
|
-
|
|
728
|
+
const { code, assessmentId, session } = req.body;
|
|
729
|
+
const db = getDatabase();
|
|
730
|
+
const firestore = getFirestore();
|
|
731
|
+
// Update the session
|
|
732
|
+
await firestore.doc(db.SESSION.DOC(code.toUpperCase())).update({
|
|
733
|
+
...session,
|
|
734
|
+
assessmentId,
|
|
735
|
+
updatedAt: new Date().getTime(),
|
|
736
|
+
});
|
|
737
|
+
return sendSuccess(res, undefined, "Session updated successfully");
|
|
741
738
|
}
|
|
742
739
|
catch (error) {
|
|
743
|
-
console.error("Error
|
|
744
|
-
return sendError(res, 500, "Failed to
|
|
740
|
+
console.error("Error updating session:", error);
|
|
741
|
+
return sendError(res, 500, "Failed to update session");
|
|
745
742
|
}
|
|
746
743
|
});
|
|
747
744
|
/**
|
|
748
745
|
* @openapi
|
|
749
|
-
* /
|
|
746
|
+
* /plan:
|
|
750
747
|
* post:
|
|
751
748
|
* tags:
|
|
752
749
|
* - Teacher
|
|
753
|
-
* summary:
|
|
754
|
-
* description:
|
|
750
|
+
* summary: Plan students
|
|
751
|
+
* description: Plans students for delivery.
|
|
755
752
|
* requestBody:
|
|
756
753
|
* required: true
|
|
757
754
|
* content:
|
|
758
755
|
* application/json:
|
|
759
756
|
* schema:
|
|
760
757
|
* type: object
|
|
761
|
-
* required:
|
|
762
|
-
* - code
|
|
763
|
-
* - studentId
|
|
764
758
|
* properties:
|
|
765
|
-
*
|
|
766
|
-
* type:
|
|
767
|
-
*
|
|
768
|
-
*
|
|
759
|
+
* count:
|
|
760
|
+
* type: number
|
|
761
|
+
* description: Number of students to plan
|
|
762
|
+
* deliveryCodes:
|
|
763
|
+
* type: array
|
|
764
|
+
* items:
|
|
765
|
+
* type: string
|
|
766
|
+
* description: Array of delivery codes
|
|
769
767
|
* responses:
|
|
770
768
|
* 200:
|
|
771
|
-
* description: Successfully
|
|
772
|
-
* 400:
|
|
773
|
-
* description: Invalid request data
|
|
774
|
-
* 401:
|
|
775
|
-
* description: Unauthorized
|
|
769
|
+
* description: Successfully planned students.
|
|
776
770
|
* 500:
|
|
777
771
|
* description: Internal server error
|
|
778
772
|
*/
|
|
779
|
-
|
|
773
|
+
teacherRouter.post("/plan", [
|
|
780
774
|
generalRateLimit,
|
|
781
|
-
body("
|
|
782
|
-
|
|
783
|
-
|
|
775
|
+
body("count")
|
|
776
|
+
.optional()
|
|
777
|
+
.isNumeric()
|
|
778
|
+
.withMessage("Count must be a number"),
|
|
779
|
+
body("deliveryCodes")
|
|
780
|
+
.optional()
|
|
781
|
+
.isArray()
|
|
782
|
+
.withMessage("Delivery codes must be an array"),
|
|
783
|
+
handleValidationErrors,
|
|
784
|
+
requireTeacherAuth,
|
|
785
|
+
], async (req, res) => {
|
|
786
|
+
try {
|
|
787
|
+
const { count, deliveryCodes } = req.body;
|
|
788
|
+
// This would need to implement the planning logic
|
|
789
|
+
// For now, returning a placeholder implementation
|
|
790
|
+
return sendSuccess(res, [], "Students planned successfully");
|
|
791
|
+
}
|
|
792
|
+
catch (error) {
|
|
793
|
+
console.error("Error planning students:", error);
|
|
794
|
+
return sendError(res, 500, "Failed to plan students");
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
/**
|
|
798
|
+
* @openapi
|
|
799
|
+
* /students:
|
|
800
|
+
* get:
|
|
801
|
+
* tags:
|
|
802
|
+
* - Teacher
|
|
803
|
+
* summary: Get sessions
|
|
804
|
+
* description: Retrieves all sessions for the teacher.
|
|
805
|
+
* responses:
|
|
806
|
+
* 200:
|
|
807
|
+
* description: Successfully retrieved sessions.
|
|
808
|
+
* 500:
|
|
809
|
+
* description: Internal server error
|
|
810
|
+
*/
|
|
811
|
+
teacherRouter.get("/students", [generalRateLimit, requireTeacherAuth], async (req, res) => {
|
|
812
|
+
try {
|
|
813
|
+
const db = getDatabase();
|
|
814
|
+
const firestore = getFirestore();
|
|
815
|
+
// Get sessions created by this teacher
|
|
816
|
+
const sessionsQuery = firestore
|
|
817
|
+
.collection(db.SESSION.COLLECTION())
|
|
818
|
+
.where("createdBy", "==", req.user.uid);
|
|
819
|
+
const sessionsSnapshot = await sessionsQuery.get();
|
|
820
|
+
const sessions = sessionsSnapshot.docs.map((doc) => doc.data());
|
|
821
|
+
return sendSuccess(res, sessions);
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
console.error("Error retrieving sessions:", error);
|
|
825
|
+
return sendError(res, 500, "Failed to retrieve sessions");
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
/**
|
|
829
|
+
* @openapi
|
|
830
|
+
* /assessment/{assessmentId}/csv:
|
|
831
|
+
* get:
|
|
832
|
+
* tags:
|
|
833
|
+
* - Teacher
|
|
834
|
+
* summary: Download results by assessment ID
|
|
835
|
+
* description: Downloads results as CSV for a specific assessment.
|
|
836
|
+
* parameters:
|
|
837
|
+
* - in: path
|
|
838
|
+
* name: assessmentId
|
|
839
|
+
* required: true
|
|
840
|
+
* schema:
|
|
841
|
+
* type: string
|
|
842
|
+
* description: The assessment ID
|
|
843
|
+
* responses:
|
|
844
|
+
* 200:
|
|
845
|
+
* description: Successfully downloaded results.
|
|
846
|
+
* content:
|
|
847
|
+
* text/csv:
|
|
848
|
+
* schema:
|
|
849
|
+
* type: string
|
|
850
|
+
* format: binary
|
|
851
|
+
* 500:
|
|
852
|
+
* description: Internal server error
|
|
853
|
+
*/
|
|
854
|
+
teacherRouter.get("/assessment/:assessmentId/csv", [
|
|
855
|
+
generalRateLimit,
|
|
856
|
+
param("assessmentId").custom((value) => {
|
|
857
|
+
if (!validateAssessmentId(value)) {
|
|
858
|
+
throw new Error("Invalid assessment ID format");
|
|
784
859
|
}
|
|
785
860
|
return true;
|
|
786
861
|
}),
|
|
787
|
-
body("identification")
|
|
788
|
-
.isString()
|
|
789
|
-
.trim()
|
|
790
|
-
.isLength({ min: 1, max: 100 })
|
|
791
|
-
.withMessage("identification must be 1-100 characters"),
|
|
792
862
|
handleValidationErrors,
|
|
793
863
|
requireTeacherAuth,
|
|
794
864
|
], async (req, res) => {
|
|
795
865
|
try {
|
|
796
|
-
const {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
866
|
+
const { assessmentId } = req.params;
|
|
867
|
+
// This would need to implement the CSV export logic
|
|
868
|
+
// For now, returning a placeholder
|
|
869
|
+
res.setHeader("Content-Type", "text/csv");
|
|
870
|
+
res.setHeader("Content-Disposition", `attachment; filename="assessment-${assessmentId}.csv"`);
|
|
871
|
+
res.send("Assessment ID,Student ID,Score\n"); // Placeholder CSV
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
console.error("Error downloading results:", error);
|
|
875
|
+
return sendError(res, 500, "Failed to download results");
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
/**
|
|
879
|
+
* @openapi
|
|
880
|
+
* /feedback:
|
|
881
|
+
* post:
|
|
882
|
+
* tags:
|
|
883
|
+
* - Teacher
|
|
884
|
+
* summary: Submit teacher feedback
|
|
885
|
+
* description: Endpoint for teachers to submit feedback with optional screenshot
|
|
886
|
+
* requestBody:
|
|
887
|
+
* required: true
|
|
888
|
+
* content:
|
|
889
|
+
* multipart/form-data:
|
|
890
|
+
* schema:
|
|
891
|
+
* type: object
|
|
892
|
+
* required:
|
|
893
|
+
* - type
|
|
894
|
+
* - description
|
|
895
|
+
* - feedbackId
|
|
896
|
+
* properties:
|
|
897
|
+
* type:
|
|
898
|
+
* type: string
|
|
899
|
+
* description: Type of feedback
|
|
900
|
+
* description:
|
|
901
|
+
* type: string
|
|
902
|
+
* description: Feedback description
|
|
903
|
+
* feedbackId:
|
|
904
|
+
* type: string
|
|
905
|
+
* description: Unique feedback identifier
|
|
906
|
+
* email:
|
|
907
|
+
* type: string
|
|
908
|
+
* description: User email (optional)
|
|
909
|
+
* pageUrl:
|
|
910
|
+
* type: string
|
|
911
|
+
* description: URL of the page where feedback was submitted
|
|
912
|
+
* screenshot:
|
|
913
|
+
* type: string
|
|
914
|
+
* format: binary
|
|
915
|
+
* description: Optional screenshot file
|
|
916
|
+
* responses:
|
|
917
|
+
* 200:
|
|
918
|
+
* description: Feedback submitted successfully
|
|
919
|
+
* 400:
|
|
920
|
+
* description: Missing required fields or validation error
|
|
921
|
+
* 401:
|
|
922
|
+
* description: Authentication required
|
|
923
|
+
* 500:
|
|
924
|
+
* description: Server error
|
|
925
|
+
*/
|
|
926
|
+
teacherRouter.post("/feedback", [generalRateLimit, requireTeacherAuth], async (req, res) => {
|
|
927
|
+
try {
|
|
928
|
+
const userId = req.user.uid;
|
|
929
|
+
const { data, fileBuffer } = await processFeedbackSubmission(req);
|
|
930
|
+
// Validate the feedback data
|
|
931
|
+
const validationError = validateFeedbackData(data);
|
|
932
|
+
if (validationError) {
|
|
933
|
+
return sendError(res, 400, validationError);
|
|
934
|
+
}
|
|
935
|
+
// Save feedback with teacher user type
|
|
936
|
+
await saveFeedback(data, userId, 'teacher', fileBuffer);
|
|
937
|
+
return sendSuccess(res, undefined, "Feedback submitted successfully");
|
|
800
938
|
}
|
|
801
939
|
catch (error) {
|
|
802
|
-
console.error("Error
|
|
803
|
-
return sendError(res, 500, "
|
|
940
|
+
console.error("Error submitting teacher feedback:", error);
|
|
941
|
+
return sendError(res, 500, "An unexpected error occurred");
|
|
804
942
|
}
|
|
805
943
|
});
|
|
944
|
+
app.use("/teacher", teacherRouter);
|
|
806
945
|
}
|
|
807
946
|
//# sourceMappingURL=qti-teacher.js.map
|