@citolab/qti-backend-firebase 0.0.3
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/LICENSE.md +674 -0
- package/dist/api/app-specific/base/application-specific-base.d.ts +45 -0
- package/dist/api/app-specific/base/application-specific-base.d.ts.map +1 -0
- package/dist/api/app-specific/base/application-specific-base.js +503 -0
- package/dist/api/app-specific/base/application-specific-base.js.map +1 -0
- package/dist/api/app-specific/base/cheerio-helper.d.ts +5 -0
- package/dist/api/app-specific/base/cheerio-helper.d.ts.map +1 -0
- package/dist/api/app-specific/base/cheerio-helper.js +16 -0
- package/dist/api/app-specific/base/cheerio-helper.js.map +1 -0
- package/dist/api/app-specific/baseImplementation.d.ts +6 -0
- package/dist/api/app-specific/baseImplementation.d.ts.map +1 -0
- package/dist/api/app-specific/baseImplementation.js +11 -0
- package/dist/api/app-specific/baseImplementation.js.map +1 -0
- package/dist/api/app-specific/index.d.ts +4 -0
- package/dist/api/app-specific/index.d.ts.map +1 -0
- package/dist/api/app-specific/index.js +4 -0
- package/dist/api/app-specific/index.js.map +1 -0
- package/dist/api/app-specific/interface/IApplicationSpecific.d.ts +31 -0
- package/dist/api/app-specific/interface/IApplicationSpecific.d.ts.map +1 -0
- package/dist/api/app-specific/interface/IApplicationSpecific.js +2 -0
- package/dist/api/app-specific/interface/IApplicationSpecific.js.map +1 -0
- package/dist/api/app-specific/specific.d.ts +1 -0
- package/dist/api/app-specific/specific.d.ts.map +1 -0
- package/dist/api/app-specific/specific.js +31 -0
- package/dist/api/app-specific/specific.js.map +1 -0
- package/dist/api/index.d.ts +3 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +15 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/qti-data.d.ts +4 -0
- package/dist/api/qti-data.d.ts.map +1 -0
- package/dist/api/qti-data.js +622 -0
- package/dist/api/qti-data.js.map +1 -0
- package/dist/api/qti-resources.d.ts +4 -0
- package/dist/api/qti-resources.d.ts.map +1 -0
- package/dist/api/qti-resources.js +348 -0
- package/dist/api/qti-resources.js.map +1 -0
- package/dist/api/qti-teacher.d.ts +6 -0
- package/dist/api/qti-teacher.d.ts.map +1 -0
- package/dist/api/qti-teacher.js +807 -0
- package/dist/api/qti-teacher.js.map +1 -0
- package/dist/api/qti-tools.d.ts +3 -0
- package/dist/api/qti-tools.d.ts.map +1 -0
- package/dist/api/qti-tools.js +450 -0
- package/dist/api/qti-tools.js.map +1 -0
- package/dist/console.d.ts +2 -0
- package/dist/console.d.ts.map +1 -0
- package/dist/console.js +30 -0
- package/dist/console.js.map +1 -0
- package/dist/express-test.d.ts +2 -0
- package/dist/express-test.d.ts.map +1 -0
- package/dist/express-test.js +19 -0
- package/dist/express-test.js.map +1 -0
- package/dist/helpers/ci-bootstap.d.ts +4 -0
- package/dist/helpers/ci-bootstap.d.ts.map +1 -0
- package/dist/helpers/ci-bootstap.js +96 -0
- package/dist/helpers/ci-bootstap.js.map +1 -0
- package/dist/helpers/database.d.ts +110 -0
- package/dist/helpers/database.d.ts.map +1 -0
- package/dist/helpers/database.js +151 -0
- package/dist/helpers/database.js.map +1 -0
- package/dist/helpers/db.d.ts +3 -0
- package/dist/helpers/db.d.ts.map +1 -0
- package/dist/helpers/db.js +10 -0
- package/dist/helpers/db.js.map +1 -0
- package/dist/helpers/endpoint-helpers.d.ts +33 -0
- package/dist/helpers/endpoint-helpers.d.ts.map +1 -0
- package/dist/helpers/endpoint-helpers.js +149 -0
- package/dist/helpers/endpoint-helpers.js.map +1 -0
- package/dist/helpers/firebase.d.ts +7 -0
- package/dist/helpers/firebase.d.ts.map +1 -0
- package/dist/helpers/firebase.js +15 -0
- package/dist/helpers/firebase.js.map +1 -0
- package/dist/helpers/index.d.ts +11 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +11 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/helpers/local-helpers.d.ts +2 -0
- package/dist/helpers/local-helpers.d.ts.map +1 -0
- package/dist/helpers/local-helpers.js +11 -0
- package/dist/helpers/local-helpers.js.map +1 -0
- package/dist/helpers/logic.d.ts +73 -0
- package/dist/helpers/logic.d.ts.map +1 -0
- package/dist/helpers/logic.js +759 -0
- package/dist/helpers/logic.js.map +1 -0
- package/dist/helpers/package-upload.d.ts +16 -0
- package/dist/helpers/package-upload.d.ts.map +1 -0
- package/dist/helpers/package-upload.js +160 -0
- package/dist/helpers/package-upload.js.map +1 -0
- package/dist/helpers/package.d.ts +32 -0
- package/dist/helpers/package.d.ts.map +1 -0
- package/dist/helpers/package.js +373 -0
- package/dist/helpers/package.js.map +1 -0
- package/dist/helpers/request-headers.d.ts +27 -0
- package/dist/helpers/request-headers.d.ts.map +1 -0
- package/dist/helpers/request-headers.js +144 -0
- package/dist/helpers/request-headers.js.map +1 -0
- package/dist/helpers/storage.d.ts +10 -0
- package/dist/helpers/storage.d.ts.map +1 -0
- package/dist/helpers/storage.js +132 -0
- package/dist/helpers/storage.js.map +1 -0
- package/dist/helpers/utils.d.ts +42 -0
- package/dist/helpers/utils.d.ts.map +1 -0
- package/dist/helpers/utils.js +228 -0
- package/dist/helpers/utils.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +14 -0
- package/dist/main.js.map +1 -0
- package/package.json +101 -0
- package/readme.md +11 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import { authenticateTeacher, getAppId } from "../helpers/request-headers";
|
|
2
|
+
import { createSession, createSessionByIdentifications, deleteStudent, getApplication, getItemsStats, getPlannedStudents, resetSession, updateItemStatResponseScore, updateStudent, updateStudentSessionState, } from "./../helpers/logic";
|
|
3
|
+
import { getDatabase } from "../helpers/db";
|
|
4
|
+
import { getFirestore } from "../helpers/firebase";
|
|
5
|
+
import { body, param, query } from "express-validator";
|
|
6
|
+
import { generalRateLimit, handleValidationErrors, heavyOperationLimit, sendError, sendSuccess, validateAssessmentId, } from "../helpers/endpoint-helpers";
|
|
7
|
+
import { dateId, createCode } from "../helpers";
|
|
8
|
+
import { BaseImplementation } from "./app-specific/baseImplementation";
|
|
9
|
+
// Teacher authentication middleware
|
|
10
|
+
export const requireTeacherAuth = async (req, res, next) => {
|
|
11
|
+
try {
|
|
12
|
+
const result = await authenticateTeacher(req, res);
|
|
13
|
+
if (!result || result.handled) {
|
|
14
|
+
return sendError(res, 401, "Teacher authentication required");
|
|
15
|
+
}
|
|
16
|
+
req.user = result.user;
|
|
17
|
+
req.appId = getAppId(req);
|
|
18
|
+
next();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error("Teacher authentication error:", error);
|
|
22
|
+
return sendError(res, 500, "Authentication failed");
|
|
23
|
+
}
|
|
24
|
+
};
|
|
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
|
+
// Validation helpers
|
|
52
|
+
const validateCode = (code) => {
|
|
53
|
+
return (typeof code === "string" &&
|
|
54
|
+
code.length > 0 &&
|
|
55
|
+
code.length <= 50 &&
|
|
56
|
+
/^[a-zA-Z0-9_-]+$/.test(code));
|
|
57
|
+
};
|
|
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
|
+
export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImplementation()) {
|
|
81
|
+
/**
|
|
82
|
+
* @openapi
|
|
83
|
+
* /teacher/startGroupDelivery:
|
|
84
|
+
* post:
|
|
85
|
+
* tags:
|
|
86
|
+
* - Teacher
|
|
87
|
+
* summary: Create a new group delivery/activity
|
|
88
|
+
* description: Creates a new activity code for group delivery sessions.
|
|
89
|
+
* requestBody:
|
|
90
|
+
* required: true
|
|
91
|
+
* content:
|
|
92
|
+
* application/json:
|
|
93
|
+
* schema:
|
|
94
|
+
* type: object
|
|
95
|
+
* required:
|
|
96
|
+
* - assessmentId
|
|
97
|
+
* properties:
|
|
98
|
+
* assessmentId:
|
|
99
|
+
* type: string
|
|
100
|
+
* description: The ID of the assessment for this group delivery
|
|
101
|
+
* responses:
|
|
102
|
+
* 200:
|
|
103
|
+
* description: Returns the created activity code
|
|
104
|
+
* content:
|
|
105
|
+
* application/json:
|
|
106
|
+
* schema:
|
|
107
|
+
* type: object
|
|
108
|
+
* properties:
|
|
109
|
+
* groupCode:
|
|
110
|
+
* type: string
|
|
111
|
+
* description: The generated activity code
|
|
112
|
+
* assessmentId:
|
|
113
|
+
* type: string
|
|
114
|
+
* description: The assessment ID
|
|
115
|
+
* 401:
|
|
116
|
+
* description: Unauthorized
|
|
117
|
+
* 500:
|
|
118
|
+
* description: Internal server error
|
|
119
|
+
*/
|
|
120
|
+
app.post("/teacher/startGroupDelivery", [
|
|
121
|
+
heavyOperationLimit,
|
|
122
|
+
body("assessmentId").notEmpty().withMessage("Assessment ID is required"),
|
|
123
|
+
handleValidationErrors,
|
|
124
|
+
requireTeacherAuth,
|
|
125
|
+
], async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const { assessmentId } = req.body;
|
|
128
|
+
const db = getDatabase(req.appId);
|
|
129
|
+
const code = await createDelivery(db);
|
|
130
|
+
if (!code) {
|
|
131
|
+
return sendError(res, 500, "Failed to create activity code");
|
|
132
|
+
}
|
|
133
|
+
const firestore = getFirestore();
|
|
134
|
+
// Create the activity document
|
|
135
|
+
await firestore.doc(db.GROUP_DELIVERY.DOC(code)).set({
|
|
136
|
+
groupCode: code,
|
|
137
|
+
assessmentId,
|
|
138
|
+
created: new Date().toISOString(),
|
|
139
|
+
teacherId: req.user.uid,
|
|
140
|
+
appId: req.appId,
|
|
141
|
+
});
|
|
142
|
+
return sendSuccess(res, { groupCode: code, assessmentId }, "Group delivery created successfully");
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error("Error creating group delivery:", error);
|
|
146
|
+
return sendError(res, 500, "Failed to create group delivery");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
/**
|
|
150
|
+
* @openapi
|
|
151
|
+
* /teacher/assessments:
|
|
152
|
+
* get:
|
|
153
|
+
* tags:
|
|
154
|
+
* - Teacher
|
|
155
|
+
* summary: Get all assessments
|
|
156
|
+
* responses:
|
|
157
|
+
* 200:
|
|
158
|
+
* description: Returns all assessments
|
|
159
|
+
* 401:
|
|
160
|
+
* description: Unauthorized
|
|
161
|
+
* 404:
|
|
162
|
+
* description: Application not found
|
|
163
|
+
* 500:
|
|
164
|
+
* description: Internal server error
|
|
165
|
+
*/
|
|
166
|
+
app.get("/teacher/assessments", [generalRateLimit, requireTeacherAuth], async (req, res) => {
|
|
167
|
+
try {
|
|
168
|
+
const db = getDatabase(req.appId);
|
|
169
|
+
const application = await getApplication(db);
|
|
170
|
+
if (!application) {
|
|
171
|
+
return sendError(res, 404, "Application not found");
|
|
172
|
+
}
|
|
173
|
+
return sendSuccess(res, { assessments: application.assessments });
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error("Error retrieving assessments:", error);
|
|
177
|
+
return sendError(res, 500, "Failed to retrieve assessments");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
/**
|
|
181
|
+
* @openapi
|
|
182
|
+
* /teacher/students:
|
|
183
|
+
* get:
|
|
184
|
+
* tags:
|
|
185
|
+
* - Teacher
|
|
186
|
+
* summary: Get planned sessions
|
|
187
|
+
* responses:
|
|
188
|
+
* 200:
|
|
189
|
+
* description: Returns planned sessions
|
|
190
|
+
* 401:
|
|
191
|
+
* description: Unauthorized
|
|
192
|
+
* 500:
|
|
193
|
+
* description: Internal server error
|
|
194
|
+
*/
|
|
195
|
+
app.get("/teacher/students", [generalRateLimit, requireTeacherAuth], async (req, res) => {
|
|
196
|
+
try {
|
|
197
|
+
const db = getDatabase(req.appId);
|
|
198
|
+
const studentSessionInfos = await getPlannedStudents(db, req.user.uid);
|
|
199
|
+
return sendSuccess(res, studentSessionInfos);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
console.error("Error retrieving planned students:", error);
|
|
203
|
+
return sendError(res, 500, "Failed to retrieve planned students");
|
|
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");
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
/**
|
|
231
|
+
* @openapi
|
|
232
|
+
* /teacher/log:
|
|
233
|
+
* post:
|
|
234
|
+
* tags:
|
|
235
|
+
* - Teacher
|
|
236
|
+
* summary: Log teacher activity
|
|
237
|
+
* description: Logs teacher activity data for debugging and analytics purposes.
|
|
238
|
+
* requestBody:
|
|
239
|
+
* required: true
|
|
240
|
+
* content:
|
|
241
|
+
* application/json:
|
|
242
|
+
* schema:
|
|
243
|
+
* type: object
|
|
244
|
+
* required:
|
|
245
|
+
* - type
|
|
246
|
+
* - data
|
|
247
|
+
* properties:
|
|
248
|
+
* type:
|
|
249
|
+
* type: string
|
|
250
|
+
* minLength: 1
|
|
251
|
+
* maxLength: 100
|
|
252
|
+
* description: The type/category of the log entry
|
|
253
|
+
* data:
|
|
254
|
+
* description: Any data to be logged
|
|
255
|
+
* responses:
|
|
256
|
+
* 200:
|
|
257
|
+
* description: Successfully logged the data.
|
|
258
|
+
* 400:
|
|
259
|
+
* description: Invalid request data
|
|
260
|
+
* 401:
|
|
261
|
+
* description: Unauthorized
|
|
262
|
+
* 500:
|
|
263
|
+
* description: Internal server error
|
|
264
|
+
*/
|
|
265
|
+
app.post("/teacher/log", [
|
|
266
|
+
generalRateLimit,
|
|
267
|
+
body("type")
|
|
268
|
+
.isString()
|
|
269
|
+
.trim()
|
|
270
|
+
.isLength({ min: 1, max: 100 })
|
|
271
|
+
.withMessage("Type must be a string between 1-100 characters"),
|
|
272
|
+
body("data").exists().withMessage("Data field is required"),
|
|
273
|
+
handleValidationErrors,
|
|
274
|
+
requireTeacherAuth,
|
|
275
|
+
], async (req, res) => {
|
|
276
|
+
try {
|
|
277
|
+
const { type, data } = req.body;
|
|
278
|
+
const db = getDatabase(req.appId);
|
|
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
|
+
};
|
|
287
|
+
const firestore = getFirestore();
|
|
288
|
+
await firestore
|
|
289
|
+
.doc(db.TEACHER.LOG.DOC(req.user.uid, dateId()))
|
|
290
|
+
.set(logEntry);
|
|
291
|
+
return sendSuccess(res, undefined, "Log entry created successfully");
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
console.error("Error creating log entry:", error);
|
|
295
|
+
return sendError(res, 500, "Failed to create log entry");
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
/**
|
|
299
|
+
* @openapi
|
|
300
|
+
* /teacher/plan:
|
|
301
|
+
* post:
|
|
302
|
+
* tags:
|
|
303
|
+
* - Teacher
|
|
304
|
+
* summary: Plan sessions
|
|
305
|
+
* requestBody:
|
|
306
|
+
* required: true
|
|
307
|
+
* content:
|
|
308
|
+
* application/json:
|
|
309
|
+
* schema:
|
|
310
|
+
* type: object
|
|
311
|
+
* properties:
|
|
312
|
+
* count:
|
|
313
|
+
* type: number
|
|
314
|
+
* minimum: 1
|
|
315
|
+
* maximum: 100
|
|
316
|
+
* assessmentIds:
|
|
317
|
+
* type: array
|
|
318
|
+
* items:
|
|
319
|
+
* type: string
|
|
320
|
+
* maxItems: 20
|
|
321
|
+
* responses:
|
|
322
|
+
* 200:
|
|
323
|
+
* description: Returns planned sessions
|
|
324
|
+
* 400:
|
|
325
|
+
* description: Invalid request data
|
|
326
|
+
* 401:
|
|
327
|
+
* description: Unauthorized
|
|
328
|
+
* 404:
|
|
329
|
+
* description: Application not found
|
|
330
|
+
* 500:
|
|
331
|
+
* description: Internal server error
|
|
332
|
+
*/
|
|
333
|
+
app.post("/teacher/plan", [
|
|
334
|
+
heavyOperationLimit,
|
|
335
|
+
body("count")
|
|
336
|
+
.optional()
|
|
337
|
+
.isInt({ min: 1, max: 100 })
|
|
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");
|
|
348
|
+
}
|
|
349
|
+
return true;
|
|
350
|
+
}),
|
|
351
|
+
handleValidationErrors,
|
|
352
|
+
requireTeacherAuth,
|
|
353
|
+
], async (req, res) => {
|
|
354
|
+
try {
|
|
355
|
+
const { count = 1, assessmentIds = [] } = req.body;
|
|
356
|
+
const db = getDatabase(req.appId);
|
|
357
|
+
const application = await getApplication(db);
|
|
358
|
+
if (!application) {
|
|
359
|
+
return sendError(res, 404, "Application not found");
|
|
360
|
+
}
|
|
361
|
+
const assessmentsToPlan = application.assessments.filter((a) => {
|
|
362
|
+
return (assessmentIds.length === 0 || assessmentIds.includes(a.assessmentId));
|
|
363
|
+
});
|
|
364
|
+
if (assessmentIds.length > 0 && assessmentsToPlan.length === 0) {
|
|
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");
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
console.error("Error planning sessions:", error);
|
|
372
|
+
return sendError(res, 500, "Failed to plan sessions");
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
/**
|
|
376
|
+
* @openapi
|
|
377
|
+
* /teacher/planByIdentification:
|
|
378
|
+
* post:
|
|
379
|
+
* tags:
|
|
380
|
+
* - Teacher
|
|
381
|
+
* summary: Plan sessions by identification e.g. student number
|
|
382
|
+
* requestBody:
|
|
383
|
+
* required: true
|
|
384
|
+
* content:
|
|
385
|
+
* application/json:
|
|
386
|
+
* schema:
|
|
387
|
+
* type: object
|
|
388
|
+
* required:
|
|
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
|
|
402
|
+
* responses:
|
|
403
|
+
* 200:
|
|
404
|
+
* description: Returns planned sessions
|
|
405
|
+
* 400:
|
|
406
|
+
* description: Invalid request data
|
|
407
|
+
* 401:
|
|
408
|
+
* description: Unauthorized
|
|
409
|
+
* 404:
|
|
410
|
+
* description: Application not found
|
|
411
|
+
* 500:
|
|
412
|
+
* description: Internal server error
|
|
413
|
+
*/
|
|
414
|
+
app.post("/teacher/planByIdentification", [
|
|
415
|
+
heavyOperationLimit,
|
|
416
|
+
body("identifications")
|
|
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) => {
|
|
431
|
+
if (!validateAssessmentId(value)) {
|
|
432
|
+
throw new Error("Invalid assessment ID format");
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
}),
|
|
436
|
+
handleValidationErrors,
|
|
437
|
+
requireTeacherAuth,
|
|
438
|
+
], async (req, res) => {
|
|
439
|
+
try {
|
|
440
|
+
const { identifications, assessmentIds = [] } = req.body;
|
|
441
|
+
const db = getDatabase(req.appId);
|
|
442
|
+
const application = await getApplication(db);
|
|
443
|
+
if (!application) {
|
|
444
|
+
return sendError(res, 404, "Application not found");
|
|
445
|
+
}
|
|
446
|
+
const assessmentsToPlan = application.assessments.filter((a) => {
|
|
447
|
+
return (assessmentIds.length === 0 || assessmentIds.includes(a.assessmentId));
|
|
448
|
+
});
|
|
449
|
+
if (assessmentIds.length > 0 && assessmentsToPlan.length === 0) {
|
|
450
|
+
return sendError(res, 400, "No valid assessments found for the provided IDs");
|
|
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");
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
console.error("Error planning sessions by identification:", error);
|
|
457
|
+
return sendError(res, 500, "Failed to plan sessions by identification");
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
/**
|
|
461
|
+
* @openapi
|
|
462
|
+
* /teacher/assessment/:assessmentId/itemStats/:itemIdentifier:
|
|
463
|
+
* post:
|
|
464
|
+
* tags:
|
|
465
|
+
* - Teacher
|
|
466
|
+
* summary: Set score for an item response
|
|
467
|
+
* parameters:
|
|
468
|
+
* - in: path
|
|
469
|
+
* name: itemIdentifier
|
|
470
|
+
* required: true
|
|
471
|
+
* schema:
|
|
472
|
+
* type: string
|
|
473
|
+
* - in: path
|
|
474
|
+
* name: assessmentId
|
|
475
|
+
* required: true
|
|
476
|
+
* schema:
|
|
477
|
+
* type: string
|
|
478
|
+
* requestBody:
|
|
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]
|
|
497
|
+
* responses:
|
|
498
|
+
* 200:
|
|
499
|
+
* description: Score updated successfully
|
|
500
|
+
* 400:
|
|
501
|
+
* description: Invalid request data
|
|
502
|
+
* 401:
|
|
503
|
+
* description: Unauthorized
|
|
504
|
+
* 500:
|
|
505
|
+
* description: Internal server error
|
|
506
|
+
*/
|
|
507
|
+
app.post("/teacher/assessment/:assessmentId/itemStats/:itemIdentifier", [
|
|
508
|
+
generalRateLimit,
|
|
509
|
+
param("itemIdentifier")
|
|
510
|
+
.isString()
|
|
511
|
+
.trim()
|
|
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");
|
|
518
|
+
}
|
|
519
|
+
return true;
|
|
520
|
+
}),
|
|
521
|
+
body("responseId")
|
|
522
|
+
.isString()
|
|
523
|
+
.trim()
|
|
524
|
+
.isLength({ min: 1, max: 100 })
|
|
525
|
+
.withMessage("Response ID is required and must be 1-100 characters"),
|
|
526
|
+
body("target")
|
|
527
|
+
.optional()
|
|
528
|
+
.custom((value) => {
|
|
529
|
+
if (value && !validateTarget(value)) {
|
|
530
|
+
throw new Error('Target must be either "teacher" or "reviewer"');
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
}),
|
|
534
|
+
handleValidationErrors,
|
|
535
|
+
requireTeacherAuth,
|
|
536
|
+
], async (req, res) => {
|
|
537
|
+
try {
|
|
538
|
+
const { assessmentId, itemIdentifier } = req.params;
|
|
539
|
+
const { responseId, scoreExternal: scoreRawValue, target = "teacher", } = req.body;
|
|
540
|
+
if (!validateTarget(target)) {
|
|
541
|
+
return sendError(res, 400, 'Invalid target. Must be "teacher" or "reviewer"');
|
|
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");
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
console.error("Error updating item score:", error);
|
|
550
|
+
return sendError(res, 500, "Failed to update item score");
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
/**
|
|
554
|
+
* @openapi
|
|
555
|
+
* /teacher/access:
|
|
556
|
+
* post:
|
|
557
|
+
* tags:
|
|
558
|
+
* - Teacher
|
|
559
|
+
* summary: Check if the teacher has access
|
|
560
|
+
* description: Authenticates the teacher and checks if they have access.
|
|
561
|
+
* responses:
|
|
562
|
+
* 200:
|
|
563
|
+
* description: Returns if the teacher has access.
|
|
564
|
+
* 401:
|
|
565
|
+
* description: Unauthorized
|
|
566
|
+
*/
|
|
567
|
+
app.post("/teacher/access", [generalRateLimit, requireTeacherAuth], async (req, res) => {
|
|
568
|
+
return sendSuccess(res, { hasAccess: true });
|
|
569
|
+
});
|
|
570
|
+
/**
|
|
571
|
+
* @openapi
|
|
572
|
+
* /teacher/session/update:
|
|
573
|
+
* post:
|
|
574
|
+
* tags:
|
|
575
|
+
* - Teacher
|
|
576
|
+
* summary: Update a student session
|
|
577
|
+
* description: Updates the session information for a student.
|
|
578
|
+
* requestBody:
|
|
579
|
+
* required: true
|
|
580
|
+
* content:
|
|
581
|
+
* application/json:
|
|
582
|
+
* schema:
|
|
583
|
+
* type: object
|
|
584
|
+
* required:
|
|
585
|
+
* - assessmentId
|
|
586
|
+
* - code
|
|
587
|
+
* - session
|
|
588
|
+
* properties:
|
|
589
|
+
* assessmentId:
|
|
590
|
+
* type: string
|
|
591
|
+
* code:
|
|
592
|
+
* type: string
|
|
593
|
+
* session:
|
|
594
|
+
* $ref: '#/components/schemas/SessionInfoTeacher'
|
|
595
|
+
* responses:
|
|
596
|
+
* 200:
|
|
597
|
+
* description: Successfully updated the session.
|
|
598
|
+
* 400:
|
|
599
|
+
* description: Invalid request data
|
|
600
|
+
* 401:
|
|
601
|
+
* description: Unauthorized
|
|
602
|
+
* 500:
|
|
603
|
+
* description: Internal server error
|
|
604
|
+
*/
|
|
605
|
+
app.post("/teacher/session/update", [
|
|
606
|
+
generalRateLimit,
|
|
607
|
+
body("assessmentId").custom((value) => {
|
|
608
|
+
if (!validateAssessmentId(value)) {
|
|
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"),
|
|
620
|
+
handleValidationErrors,
|
|
621
|
+
requireTeacherAuth,
|
|
622
|
+
], async (req, res) => {
|
|
623
|
+
try {
|
|
624
|
+
const { assessmentId, code, session } = req.body;
|
|
625
|
+
const db = getDatabase(req.appId);
|
|
626
|
+
const firestore = getFirestore();
|
|
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");
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
console.error("Error updating session:", error);
|
|
637
|
+
return sendError(res, 500, "Failed to update session");
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
/**
|
|
641
|
+
* @openapi
|
|
642
|
+
* /teacher/session/{code}:
|
|
643
|
+
* delete:
|
|
644
|
+
* tags:
|
|
645
|
+
* - Teacher
|
|
646
|
+
* summary: Delete a students session
|
|
647
|
+
* description: Deletes a specific session for a student.
|
|
648
|
+
* parameters:
|
|
649
|
+
* - in: path
|
|
650
|
+
* name: code
|
|
651
|
+
* required: true
|
|
652
|
+
* schema:
|
|
653
|
+
* type: string
|
|
654
|
+
* description: The session code.
|
|
655
|
+
* responses:
|
|
656
|
+
* 200:
|
|
657
|
+
* description: Successfully deleted the session.
|
|
658
|
+
* 400:
|
|
659
|
+
* description: Invalid code format
|
|
660
|
+
* 401:
|
|
661
|
+
* description: Unauthorized
|
|
662
|
+
* 500:
|
|
663
|
+
* description: Internal server error
|
|
664
|
+
*/
|
|
665
|
+
app.delete("/teacher/session/:code", [
|
|
666
|
+
generalRateLimit,
|
|
667
|
+
param("code").custom((value) => {
|
|
668
|
+
if (!validateCode(value)) {
|
|
669
|
+
throw new Error("Invalid code format");
|
|
670
|
+
}
|
|
671
|
+
return true;
|
|
672
|
+
}),
|
|
673
|
+
handleValidationErrors,
|
|
674
|
+
requireTeacherAuth,
|
|
675
|
+
], async (req, res) => {
|
|
676
|
+
try {
|
|
677
|
+
const { code } = req.params;
|
|
678
|
+
const db = getDatabase(req.appId);
|
|
679
|
+
await deleteStudent(db, req.user.uid, code);
|
|
680
|
+
return sendSuccess(res, undefined, "Session deleted successfully");
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
console.error("Error deleting session:", error);
|
|
684
|
+
return sendError(res, 500, "Failed to delete session");
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
/**
|
|
688
|
+
* @openapi
|
|
689
|
+
* /teacher/session/reset:
|
|
690
|
+
* post:
|
|
691
|
+
* tags:
|
|
692
|
+
* - Teacher
|
|
693
|
+
* summary: Reset a teacher's session
|
|
694
|
+
* description: Resets the session information for a student.
|
|
695
|
+
* requestBody:
|
|
696
|
+
* required: true
|
|
697
|
+
* content:
|
|
698
|
+
* application/json:
|
|
699
|
+
* schema:
|
|
700
|
+
* type: object
|
|
701
|
+
* required:
|
|
702
|
+
* - code
|
|
703
|
+
* - assessmentId
|
|
704
|
+
* properties:
|
|
705
|
+
* code:
|
|
706
|
+
* type: string
|
|
707
|
+
* assessmentId:
|
|
708
|
+
* type: string
|
|
709
|
+
* responses:
|
|
710
|
+
* 200:
|
|
711
|
+
* description: Successfully reset the session.
|
|
712
|
+
* 400:
|
|
713
|
+
* description: Invalid request data
|
|
714
|
+
* 401:
|
|
715
|
+
* description: Unauthorized
|
|
716
|
+
* 500:
|
|
717
|
+
* description: Internal server error
|
|
718
|
+
*/
|
|
719
|
+
app.post("/teacher/session/reset", [
|
|
720
|
+
generalRateLimit,
|
|
721
|
+
body("code").custom((value) => {
|
|
722
|
+
if (!validateCode(value)) {
|
|
723
|
+
throw new Error("Invalid code format");
|
|
724
|
+
}
|
|
725
|
+
return true;
|
|
726
|
+
}),
|
|
727
|
+
body("assessmentId").custom((value) => {
|
|
728
|
+
if (!validateAssessmentId(value)) {
|
|
729
|
+
throw new Error("Invalid assessment ID format");
|
|
730
|
+
}
|
|
731
|
+
return true;
|
|
732
|
+
}),
|
|
733
|
+
handleValidationErrors,
|
|
734
|
+
requireTeacherAuth,
|
|
735
|
+
], async (req, res) => {
|
|
736
|
+
try {
|
|
737
|
+
const { code, assessmentId } = req.body;
|
|
738
|
+
const db = getDatabase(req.appId);
|
|
739
|
+
await resetSession(db, req.user.uid, code, assessmentId);
|
|
740
|
+
return sendSuccess(res, undefined, "Session reset successfully");
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
console.error("Error resetting session:", error);
|
|
744
|
+
return sendError(res, 500, "Failed to reset session");
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
/**
|
|
748
|
+
* @openapi
|
|
749
|
+
* /teacher/student/update:
|
|
750
|
+
* post:
|
|
751
|
+
* tags:
|
|
752
|
+
* - Teacher
|
|
753
|
+
* summary: Update student information
|
|
754
|
+
* description: Updates student information for a specific session.
|
|
755
|
+
* requestBody:
|
|
756
|
+
* required: true
|
|
757
|
+
* content:
|
|
758
|
+
* application/json:
|
|
759
|
+
* schema:
|
|
760
|
+
* type: object
|
|
761
|
+
* required:
|
|
762
|
+
* - code
|
|
763
|
+
* - studentId
|
|
764
|
+
* properties:
|
|
765
|
+
* code:
|
|
766
|
+
* type: string
|
|
767
|
+
* studentId:
|
|
768
|
+
* type: string
|
|
769
|
+
* responses:
|
|
770
|
+
* 200:
|
|
771
|
+
* description: Successfully updated student information.
|
|
772
|
+
* 400:
|
|
773
|
+
* description: Invalid request data
|
|
774
|
+
* 401:
|
|
775
|
+
* description: Unauthorized
|
|
776
|
+
* 500:
|
|
777
|
+
* description: Internal server error
|
|
778
|
+
*/
|
|
779
|
+
app.post("/teacher/student/update", [
|
|
780
|
+
generalRateLimit,
|
|
781
|
+
body("code").custom((value) => {
|
|
782
|
+
if (!validateCode(value)) {
|
|
783
|
+
throw new Error("Invalid code format");
|
|
784
|
+
}
|
|
785
|
+
return true;
|
|
786
|
+
}),
|
|
787
|
+
body("identification")
|
|
788
|
+
.isString()
|
|
789
|
+
.trim()
|
|
790
|
+
.isLength({ min: 1, max: 100 })
|
|
791
|
+
.withMessage("identification must be 1-100 characters"),
|
|
792
|
+
handleValidationErrors,
|
|
793
|
+
requireTeacherAuth,
|
|
794
|
+
], async (req, res) => {
|
|
795
|
+
try {
|
|
796
|
+
const { code, identification } = req.body;
|
|
797
|
+
const db = getDatabase(req.appId);
|
|
798
|
+
await updateStudent(db, req.user.uid, code, identification);
|
|
799
|
+
return sendSuccess(res, undefined, "Student updated successfully");
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
console.error("Error updating student:", error);
|
|
803
|
+
return sendError(res, 500, "Failed to update student");
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
//# sourceMappingURL=qti-teacher.js.map
|