@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,759 @@
|
|
|
1
|
+
import { getAuth, getFirestore, getStorage } from "./firebase";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import mime from "mime";
|
|
4
|
+
import { createCode, removeDoubleSlashes } from "./utils";
|
|
5
|
+
import { DATABASE } from "./database";
|
|
6
|
+
import { deleteFilesRecursive } from "./storage";
|
|
7
|
+
import NodeCache from "node-cache";
|
|
8
|
+
export async function uploadFolderToStorage(folder, storageDestinationPath, uploadFiles, subfolder) {
|
|
9
|
+
const storage = getStorage();
|
|
10
|
+
const bucket = storage.bucket();
|
|
11
|
+
const fullFolder = subfolder ? `${folder}/${subfolder}` : folder;
|
|
12
|
+
const files = await fs.readdir(fullFolder);
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
if (file === ".DS_Store") {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const filePath = `${fullFolder}/${file}`;
|
|
18
|
+
const stat = await fs.stat(filePath);
|
|
19
|
+
const fileStructure = removeDoubleSlashes(filePath.replace(folder, ""));
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
// Recursively upload files in subfolder
|
|
22
|
+
await uploadFolderToStorage(folder, `${storageDestinationPath}/${file}`, uploadFiles, fileStructure);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.log(`uploading ${filePath}`);
|
|
26
|
+
uploadFiles.push(bucket.upload(`${fullFolder}/${file}`, {
|
|
27
|
+
destination: `${storageDestinationPath}/${file}`,
|
|
28
|
+
metadata: {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
contentType: mime.getType(file),
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const applicationInfoCache = new NodeCache({
|
|
37
|
+
stdTTL: 60 * 5, // 5 minutes
|
|
38
|
+
});
|
|
39
|
+
const userIdCode = new NodeCache({
|
|
40
|
+
stdTTL: 60 * 20, // 20 minutes
|
|
41
|
+
});
|
|
42
|
+
export const getStudentSessionInfo = async (code, db, userId) => {
|
|
43
|
+
if (!code || !db)
|
|
44
|
+
return null;
|
|
45
|
+
const application = await getApplication(db);
|
|
46
|
+
if ((application?.demoCodes || []).includes(code)) {
|
|
47
|
+
const allAssessments = await getAssessments(db);
|
|
48
|
+
return {
|
|
49
|
+
appId: db.appId,
|
|
50
|
+
code,
|
|
51
|
+
sessions: allAssessments.map((a) => {
|
|
52
|
+
return {
|
|
53
|
+
assessmentId: a.assessmentId,
|
|
54
|
+
assessmentName: a.name,
|
|
55
|
+
packageId: a.packageId,
|
|
56
|
+
sessionState: "not_started",
|
|
57
|
+
};
|
|
58
|
+
}),
|
|
59
|
+
assessment: allAssessments.length ? allAssessments[0] : null,
|
|
60
|
+
state: "not_started",
|
|
61
|
+
isDemo: true,
|
|
62
|
+
teacherId: "demo-teacher",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const firestore = getFirestore();
|
|
66
|
+
const doc = await firestore.doc(db.STUDENT.DOC(code)).get();
|
|
67
|
+
if (userId && doc.exists) {
|
|
68
|
+
const firestore = getFirestore();
|
|
69
|
+
// store the mapping between the user and the code so on refresh we can get the code by the user
|
|
70
|
+
await firestore.doc(db.STUDENT_USER_ID_MAPPING.DOC(userId)).set({ code });
|
|
71
|
+
}
|
|
72
|
+
return doc.exists ? doc.data() : null;
|
|
73
|
+
};
|
|
74
|
+
export const getStudentGroupSessionInfo = async (groupCode, db, userId, studentIdentification) => {
|
|
75
|
+
if (!groupCode || !db || !userId)
|
|
76
|
+
return null;
|
|
77
|
+
const firestore = getFirestore();
|
|
78
|
+
// Check if the group code exists in the assessment codes collection
|
|
79
|
+
const groupDeliveryDoc = await firestore
|
|
80
|
+
.doc(db.GROUP_DELIVERY.DOC(groupCode))
|
|
81
|
+
.get();
|
|
82
|
+
if (!groupDeliveryDoc.exists) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const assessment = groupDeliveryDoc.data();
|
|
86
|
+
const fullAssessment = await getAssessmentTest(db, assessment.assessmentId);
|
|
87
|
+
// Create the mapping between user and code
|
|
88
|
+
await firestore.doc(db.STUDENT_USER_ID_MAPPING.DOC(userId)).set({
|
|
89
|
+
code: groupCode,
|
|
90
|
+
});
|
|
91
|
+
// Create/update student info entry
|
|
92
|
+
const studentDocRef = firestore.doc(db.STUDENT.DOC(userId)); // Assuming nested structure
|
|
93
|
+
const studentInfo = {
|
|
94
|
+
appId: db.appId || "unknown",
|
|
95
|
+
code: groupCode,
|
|
96
|
+
currentAssessmentId: "",
|
|
97
|
+
identification: studentIdentification || "",
|
|
98
|
+
sessions: [],
|
|
99
|
+
isDemo: false,
|
|
100
|
+
teacherId: fullAssessment.teacherId || "unknown_teacher",
|
|
101
|
+
userId: userId,
|
|
102
|
+
};
|
|
103
|
+
// Get assessment details
|
|
104
|
+
const assessments = await getStartableAssessments(db);
|
|
105
|
+
if (assessments && assessments.length > 0) {
|
|
106
|
+
if (!studentInfo.currentAssessmentId) {
|
|
107
|
+
studentInfo.currentAssessmentId = assessment.assessmentId;
|
|
108
|
+
}
|
|
109
|
+
studentInfo.sessions = [
|
|
110
|
+
{
|
|
111
|
+
assessmentId: assessment.assessmentId,
|
|
112
|
+
assessmentName: assessment.name,
|
|
113
|
+
packageId: assessment.packageId,
|
|
114
|
+
sessionState: "not_started",
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
await studentDocRef.set(studentInfo);
|
|
119
|
+
return studentInfo;
|
|
120
|
+
};
|
|
121
|
+
export const updateStudentSessionState = async (db, code, assessmentId, state, batch) => {
|
|
122
|
+
if (!code || !db)
|
|
123
|
+
return null;
|
|
124
|
+
const firestore = getFirestore();
|
|
125
|
+
const studentDocRef = firestore.doc(db.STUDENT.DOC(code));
|
|
126
|
+
const student = (await studentDocRef.get())?.data();
|
|
127
|
+
if (student) {
|
|
128
|
+
const updatedStudent = {
|
|
129
|
+
...student,
|
|
130
|
+
sessions: student.sessions.map((s) => s.assessmentId === assessmentId ? { ...s, sessionState: state } : s),
|
|
131
|
+
};
|
|
132
|
+
batch.set(studentDocRef, updatedStudent);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
export const createAssessmentCode = async (db, charCount, assessmentId, application) => {
|
|
136
|
+
let code = createCode(charCount);
|
|
137
|
+
let exists = !!(await codeExists(code, db));
|
|
138
|
+
while (exists) {
|
|
139
|
+
code = createCode(charCount);
|
|
140
|
+
exists =
|
|
141
|
+
(application?.demoCodes || []).includes(code) ||
|
|
142
|
+
!!(await codeExists(code, db));
|
|
143
|
+
}
|
|
144
|
+
const firestore = getFirestore();
|
|
145
|
+
const assessmentData = await firestore
|
|
146
|
+
.doc(db.ASSESSMENT.DOC(assessmentId))
|
|
147
|
+
.get();
|
|
148
|
+
if (assessmentData.exists) {
|
|
149
|
+
const assessmentInfo = assessmentData.data();
|
|
150
|
+
await firestore.doc(db.GROUP_DELIVERY.DOC(code)).set(assessmentInfo);
|
|
151
|
+
return code;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
export const createDemoCodeForAssessment = async (db, assessmentId, code) => {
|
|
158
|
+
const exists = !!(await codeExists(code, db));
|
|
159
|
+
if (exists) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const firestore = getFirestore();
|
|
163
|
+
const assessmentData = await firestore
|
|
164
|
+
.doc(db.ASSESSMENT.DOC(assessmentId))
|
|
165
|
+
.get();
|
|
166
|
+
const assessmentInfo = assessmentData.data();
|
|
167
|
+
await firestore
|
|
168
|
+
.doc(db.GROUP_DELIVERY.DOC(code))
|
|
169
|
+
.set({ ...assessmentInfo, isDemo: true });
|
|
170
|
+
return code;
|
|
171
|
+
};
|
|
172
|
+
export const checkAssessmentCode = async (db, code) => {
|
|
173
|
+
const firestore = getFirestore();
|
|
174
|
+
if (!code || !db)
|
|
175
|
+
return null;
|
|
176
|
+
const application = await getApplication(db);
|
|
177
|
+
if ((application?.demoCodes || []).includes(code)) {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
const assessmentInfo = await firestore.doc(db.GROUP_DELIVERY.DOC(code));
|
|
181
|
+
const doc = await assessmentInfo.get();
|
|
182
|
+
const assessment = doc.data();
|
|
183
|
+
return assessment;
|
|
184
|
+
};
|
|
185
|
+
export const isStartable = (asessment) => {
|
|
186
|
+
if (asessment.startFrom) {
|
|
187
|
+
const startFrom = new Date(asessment.startFrom);
|
|
188
|
+
if (startFrom > new Date()) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (asessment.endAt) {
|
|
193
|
+
const startUntil = new Date(asessment.endAt);
|
|
194
|
+
if (startUntil < new Date()) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
};
|
|
200
|
+
export const getCodeByUserId = async (db, userId) => {
|
|
201
|
+
const key = `${db.appId}-${userId}`;
|
|
202
|
+
const cached = userIdCode.get(key);
|
|
203
|
+
if (cached) {
|
|
204
|
+
return cached;
|
|
205
|
+
}
|
|
206
|
+
const firestore = getFirestore();
|
|
207
|
+
const doc = await firestore.doc(db.STUDENT_USER_ID_MAPPING.DOC(userId)).get();
|
|
208
|
+
const code = doc.exists ? doc.data().code : null;
|
|
209
|
+
if (code) {
|
|
210
|
+
userIdCode.set(key, code);
|
|
211
|
+
}
|
|
212
|
+
return code;
|
|
213
|
+
};
|
|
214
|
+
export const getTeacherIdByCode = async (db, code) => {
|
|
215
|
+
const key = `${db.appId}-${code}`;
|
|
216
|
+
const cached = userIdCode.get(key);
|
|
217
|
+
if (cached) {
|
|
218
|
+
return cached;
|
|
219
|
+
}
|
|
220
|
+
const firestore = getFirestore();
|
|
221
|
+
const doc = await firestore.doc(db.STUDENT.DOC(code)).get();
|
|
222
|
+
const teacherId = doc.exists
|
|
223
|
+
? doc.data().teacherId
|
|
224
|
+
: null;
|
|
225
|
+
if (teacherId) {
|
|
226
|
+
userIdCode.set(key, teacherId);
|
|
227
|
+
}
|
|
228
|
+
return teacherId;
|
|
229
|
+
};
|
|
230
|
+
export const getApplicationInfo = async (db) => {
|
|
231
|
+
const cached = applicationInfoCache.get(db.appId || "application");
|
|
232
|
+
if (cached) {
|
|
233
|
+
return cached;
|
|
234
|
+
}
|
|
235
|
+
const firestore = getFirestore();
|
|
236
|
+
const doc = await firestore.doc(db.APPLICATION.DOC()).get();
|
|
237
|
+
if (doc.exists) {
|
|
238
|
+
const applicationInfo = doc.data();
|
|
239
|
+
applicationInfoCache.set(db.appId || "application", applicationInfo);
|
|
240
|
+
return applicationInfo;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
export const addApplicationInfo = async (db, appInfo) => {
|
|
244
|
+
const firestore = getFirestore();
|
|
245
|
+
const docRef = firestore.doc(db.APPLICATION.DOC());
|
|
246
|
+
await docRef.set(appInfo);
|
|
247
|
+
};
|
|
248
|
+
export const getApplication = async (db) => {
|
|
249
|
+
const firestore = getFirestore();
|
|
250
|
+
const docRef = firestore.doc(db.APPLICATION.DOC());
|
|
251
|
+
const applicationRef = await docRef.get();
|
|
252
|
+
const application = applicationRef.exists
|
|
253
|
+
? applicationRef.data()
|
|
254
|
+
: {
|
|
255
|
+
name: db.appId,
|
|
256
|
+
};
|
|
257
|
+
if (application &&
|
|
258
|
+
(!application.assessments || !application.assessments.length)) {
|
|
259
|
+
const assessments = await getAssessments(db);
|
|
260
|
+
if (!applicationRef.exists && assessments.length === 0) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
application.assessments = assessments;
|
|
264
|
+
}
|
|
265
|
+
return application;
|
|
266
|
+
};
|
|
267
|
+
export const codeExists = async (code, db) => {
|
|
268
|
+
const studentSessions = await getStudentSessionInfo(code, db, undefined);
|
|
269
|
+
return studentSessions;
|
|
270
|
+
};
|
|
271
|
+
export const hasAccess = async (uid, appId) => {
|
|
272
|
+
const firestore = getFirestore();
|
|
273
|
+
const docRef = firestore.doc(new DATABASE().AUTH.CHECK_APP_RIGHTS(uid, appId.toLowerCase()));
|
|
274
|
+
const doc = await docRef.get();
|
|
275
|
+
return doc.exists;
|
|
276
|
+
};
|
|
277
|
+
// TODO: sessionState should be default on not_started. For snelkookpan specific move to specific logig
|
|
278
|
+
export const createSession = async (teacherId, db, count, assessments) => {
|
|
279
|
+
const firestore = getFirestore();
|
|
280
|
+
const application = await getApplication(db);
|
|
281
|
+
const codes = new Array(count).fill(createCode(5));
|
|
282
|
+
const studentAppSessionInfos = [];
|
|
283
|
+
const batch = firestore.batch();
|
|
284
|
+
for (let code of codes) {
|
|
285
|
+
// check if code does not exists twice in codes array
|
|
286
|
+
const isGeneratedTwice = codes.filter((c) => c === code).length > 1;
|
|
287
|
+
let exists = (await codeExists(code, db)) || isGeneratedTwice;
|
|
288
|
+
while (exists) {
|
|
289
|
+
code = createCode(5);
|
|
290
|
+
exists = !!((application?.demoCodes || []).includes(code) ||
|
|
291
|
+
(await codeExists(code, db)));
|
|
292
|
+
}
|
|
293
|
+
const studentSessionInfo = {
|
|
294
|
+
teacherId,
|
|
295
|
+
code,
|
|
296
|
+
appId: db.appId || "application",
|
|
297
|
+
sessions: assessments.map((assessment) => ({
|
|
298
|
+
assessmentId: assessment.assessmentId,
|
|
299
|
+
assessmentName: assessment.name,
|
|
300
|
+
packageId: assessment.packageId,
|
|
301
|
+
sessionState: assessment.assessmentId.startsWith("practice")
|
|
302
|
+
? "not_generated"
|
|
303
|
+
: "not_available",
|
|
304
|
+
})),
|
|
305
|
+
isDemo: false,
|
|
306
|
+
};
|
|
307
|
+
studentAppSessionInfos.push(studentSessionInfo);
|
|
308
|
+
const teacherResult = {
|
|
309
|
+
code,
|
|
310
|
+
sessions: studentSessionInfo.sessions.map((session) => ({
|
|
311
|
+
code,
|
|
312
|
+
assessmentId: session.assessmentId,
|
|
313
|
+
sessionState: session.sessionState,
|
|
314
|
+
testScore: 0,
|
|
315
|
+
})),
|
|
316
|
+
};
|
|
317
|
+
batch.set(firestore.doc(db.STUDENT.DOC(code)), studentSessionInfo);
|
|
318
|
+
// now add them to the teacher view model
|
|
319
|
+
const teacherDocPath = db.TEACHER.STUDENTS.DOC(teacherId, teacherResult.code);
|
|
320
|
+
batch.set(firestore.doc(teacherDocPath), teacherResult);
|
|
321
|
+
}
|
|
322
|
+
await batch.commit();
|
|
323
|
+
return studentAppSessionInfos;
|
|
324
|
+
};
|
|
325
|
+
export const createSessionByIdentifications = async (teacherId, db, identifications, sessionState, assessments) => {
|
|
326
|
+
const firestore = getFirestore();
|
|
327
|
+
const studentAppSessionInfos = [];
|
|
328
|
+
const batch = firestore.batch();
|
|
329
|
+
for (const identification of identifications) {
|
|
330
|
+
const exists = await codeExists(identification.identifier, db);
|
|
331
|
+
if (!exists) {
|
|
332
|
+
const studentSessionInfo = {
|
|
333
|
+
teacherId,
|
|
334
|
+
code: identification.identifier,
|
|
335
|
+
appId: db.appId || "application",
|
|
336
|
+
sessions: assessments.map((assessment) => ({
|
|
337
|
+
assessmentId: assessment.assessmentId,
|
|
338
|
+
assessmentName: assessment.name,
|
|
339
|
+
packageId: assessment.packageId,
|
|
340
|
+
sessionState,
|
|
341
|
+
})),
|
|
342
|
+
isDemo: false,
|
|
343
|
+
};
|
|
344
|
+
if (identification.password) {
|
|
345
|
+
studentSessionInfo.password = identification.password;
|
|
346
|
+
}
|
|
347
|
+
studentAppSessionInfos.push(studentSessionInfo);
|
|
348
|
+
const teacherResult = {
|
|
349
|
+
code: identification.identifier,
|
|
350
|
+
sessions: studentSessionInfo.sessions.map((session) => ({
|
|
351
|
+
code: identification.identifier,
|
|
352
|
+
assessmentId: session.assessmentId,
|
|
353
|
+
sessionState: session.sessionState,
|
|
354
|
+
testScore: 0,
|
|
355
|
+
})),
|
|
356
|
+
};
|
|
357
|
+
batch.set(firestore.doc(db.STUDENT.DOC(identification.identifier)), studentSessionInfo);
|
|
358
|
+
// now add them to the teacher view model
|
|
359
|
+
const teacherDocPath = db.TEACHER.STUDENTS.DOC(teacherId, teacherResult.code);
|
|
360
|
+
batch.set(firestore.doc(teacherDocPath), teacherResult);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
await batch.commit();
|
|
364
|
+
return studentAppSessionInfos;
|
|
365
|
+
};
|
|
366
|
+
export const getAllStudents = async (db) => {
|
|
367
|
+
const firestore = getFirestore();
|
|
368
|
+
const docs = await firestore
|
|
369
|
+
.collection(db.STUDENT.COLLECTION())
|
|
370
|
+
.listDocuments();
|
|
371
|
+
const studentInfos = [];
|
|
372
|
+
for (const doc of docs) {
|
|
373
|
+
const studentInfo = await doc.get();
|
|
374
|
+
studentInfos.push(studentInfo.data() ||
|
|
375
|
+
{ code: doc.id });
|
|
376
|
+
}
|
|
377
|
+
return studentInfos;
|
|
378
|
+
};
|
|
379
|
+
export const getAssessments = async (db) => {
|
|
380
|
+
const firestore = getFirestore();
|
|
381
|
+
const assessmentData = await firestore
|
|
382
|
+
.collection(db.ASSESSMENT.COLLECTION())
|
|
383
|
+
.get();
|
|
384
|
+
const assessments = assessmentData.docs.map((doc) => doc.data());
|
|
385
|
+
return assessments;
|
|
386
|
+
};
|
|
387
|
+
export const getStartableAssessments = async (db) => {
|
|
388
|
+
const application = await getApplication(db);
|
|
389
|
+
if (!application) {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
const startableAssessments = application.assessments.filter((a) => {
|
|
393
|
+
if (a.startFrom) {
|
|
394
|
+
const startFrom = new Date(a.startFrom);
|
|
395
|
+
if (startFrom > new Date()) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (a.endAt) {
|
|
400
|
+
const startUntil = new Date(a.endAt);
|
|
401
|
+
if (startUntil < new Date()) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return a.canStart !== false;
|
|
406
|
+
});
|
|
407
|
+
return startableAssessments;
|
|
408
|
+
};
|
|
409
|
+
export const getAssessmentTest = async (db, assessmentId) => {
|
|
410
|
+
const firestore = getFirestore();
|
|
411
|
+
const assessmentDoc = await firestore
|
|
412
|
+
.doc(db.ASSESSMENT.DOC(assessmentId))
|
|
413
|
+
.get();
|
|
414
|
+
if (!assessmentDoc.exists) {
|
|
415
|
+
throw new Error(`Assessment with ID ${assessmentId} does not exist.`);
|
|
416
|
+
}
|
|
417
|
+
const assessmentData = assessmentDoc.data();
|
|
418
|
+
if (!assessmentData) {
|
|
419
|
+
throw new Error(`Assessment with ID ${assessmentId} has no package.`);
|
|
420
|
+
}
|
|
421
|
+
return assessmentData;
|
|
422
|
+
};
|
|
423
|
+
export const getAssessmentItemRefsFromPackage = async ($assessmentTest) => {
|
|
424
|
+
//let regularItemCount = 0;
|
|
425
|
+
let itemCount = 0;
|
|
426
|
+
const assessmentItemRefs = $assessmentTest("qti-assessment-item-ref")
|
|
427
|
+
.map((i, el) => {
|
|
428
|
+
const isInfo = $assessmentTest(el)
|
|
429
|
+
.attr("category")
|
|
430
|
+
?.includes("dep-informational");
|
|
431
|
+
// if (!isInfo) {
|
|
432
|
+
// regularItemCount++;
|
|
433
|
+
// }
|
|
434
|
+
itemCount++;
|
|
435
|
+
const identifier = $assessmentTest(el).attr("identifier");
|
|
436
|
+
const sequenceNumber = itemCount; // isInfo ? 0 : regularItemCount;
|
|
437
|
+
return { identifier, sequenceNumber, isInfo };
|
|
438
|
+
})
|
|
439
|
+
.toArray();
|
|
440
|
+
return assessmentItemRefs;
|
|
441
|
+
};
|
|
442
|
+
export const getItemsStats = async (db, teacherId, assessmentId, target) => {
|
|
443
|
+
const firestore = getFirestore();
|
|
444
|
+
const assessmentDoc = await getAssessmentTest(db, assessmentId);
|
|
445
|
+
if (!assessmentDoc) {
|
|
446
|
+
throw new Error(`Assessment with ID ${assessmentId} does not exist.`);
|
|
447
|
+
}
|
|
448
|
+
if (!assessmentDoc.items || assessmentDoc.items.length === 0) {
|
|
449
|
+
throw new Error(`Assessment with ID ${assessmentId} has no items.`);
|
|
450
|
+
}
|
|
451
|
+
const itemIdentifiers = assessmentDoc.items?.map((item) => item.identifier) || [];
|
|
452
|
+
const all = await Promise.all(itemIdentifiers.map((id) => firestore
|
|
453
|
+
.collection(target === "teacher"
|
|
454
|
+
? db.TEACHER.ASSESSMENT_ITEM_STATS.RESPONSES.COLLECTION(teacherId, assessmentId, id)
|
|
455
|
+
: db.REVIEWER.ASSESSMENT_ITEM_STATS.RESPONSES.COLLECTION(teacherId, assessmentId, id))
|
|
456
|
+
.get()
|
|
457
|
+
.then((snapshot) => ({ itemIdentifier: id, docs: snapshot.docs }))));
|
|
458
|
+
const stats = [];
|
|
459
|
+
for (const { itemIdentifier, docs } of all) {
|
|
460
|
+
if (!docs?.length) {
|
|
461
|
+
// skip empty collections
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
for (const d of docs) {
|
|
465
|
+
if (!d.exists) {
|
|
466
|
+
console.warn(`Item statistics for item ${itemIdentifier} in assessment ${assessmentId} does not exist.`);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const data = d.data();
|
|
470
|
+
if (data) {
|
|
471
|
+
let itemStats = stats.find((s) => s.itemId === itemIdentifier);
|
|
472
|
+
if (!itemStats) {
|
|
473
|
+
itemStats = {
|
|
474
|
+
itemId: itemIdentifier,
|
|
475
|
+
count: 0,
|
|
476
|
+
responses: [],
|
|
477
|
+
numberCorrect: 0,
|
|
478
|
+
};
|
|
479
|
+
stats.push(itemStats);
|
|
480
|
+
}
|
|
481
|
+
itemStats.count += data.count;
|
|
482
|
+
itemStats.numberCorrect +=
|
|
483
|
+
!data.score || data.score?.toString() === "0" ? 0 : 1;
|
|
484
|
+
itemStats.responses.push(data);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return stats;
|
|
490
|
+
};
|
|
491
|
+
export const updateItemStatResponseScore = async (db, teacherId, assessmentId, itemId, responseId, scoreExternal, target) => {
|
|
492
|
+
const firestore = getFirestore();
|
|
493
|
+
await firestore
|
|
494
|
+
.doc(target === "teacher"
|
|
495
|
+
? db.TEACHER.ASSESSMENT_ITEM_STATS.RESPONSES.DOC(teacherId, assessmentId, itemId, responseId)
|
|
496
|
+
: db.REVIEWER.ASSESSMENT_ITEM_STATS.RESPONSES.DOC(teacherId, assessmentId, itemId, responseId))
|
|
497
|
+
.update({ scoreExternal });
|
|
498
|
+
};
|
|
499
|
+
export const getPlannedStudents = async (db, teacherId) => {
|
|
500
|
+
const firestore = getFirestore();
|
|
501
|
+
const studentResultCollectionRef = await firestore
|
|
502
|
+
.collection(db.TEACHER.STUDENTS.COLLECTION(teacherId))
|
|
503
|
+
.get();
|
|
504
|
+
// StudentResult.
|
|
505
|
+
const studentSessionInfo = studentResultCollectionRef.docs.map((doc) => doc.data());
|
|
506
|
+
const studentsToReturn = [];
|
|
507
|
+
let prevSessionState = "";
|
|
508
|
+
for (const student of studentSessionInfo) {
|
|
509
|
+
const sessionsToReturn = [];
|
|
510
|
+
for (const session of student.sessions) {
|
|
511
|
+
if (session.sessionState === "not_generated" &&
|
|
512
|
+
session.assessmentId === "practice" &&
|
|
513
|
+
prevSessionState === "scored") {
|
|
514
|
+
session.sessionState = "not_available";
|
|
515
|
+
}
|
|
516
|
+
else if (session.assessmentId === "practice-2" &&
|
|
517
|
+
(session.sessionState === "not_generated" ||
|
|
518
|
+
session.sessionState === "not_available") &&
|
|
519
|
+
prevSessionState === "scored") {
|
|
520
|
+
session.sessionState = "not_started";
|
|
521
|
+
}
|
|
522
|
+
prevSessionState = session.sessionState;
|
|
523
|
+
sessionsToReturn.push(session);
|
|
524
|
+
}
|
|
525
|
+
const updatedStudent = { ...student, sessions: sessionsToReturn };
|
|
526
|
+
studentsToReturn.push(updatedStudent);
|
|
527
|
+
}
|
|
528
|
+
return studentsToReturn;
|
|
529
|
+
};
|
|
530
|
+
export const getPlannedStudent = async (db, teacherId, code) => {
|
|
531
|
+
const firestore = getFirestore();
|
|
532
|
+
const studentResultRef = await firestore
|
|
533
|
+
.doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
|
|
534
|
+
.get();
|
|
535
|
+
return studentResultRef.data();
|
|
536
|
+
};
|
|
537
|
+
export const deleteStudent = async (db, teacherId, code) => {
|
|
538
|
+
const firestore = getFirestore();
|
|
539
|
+
const studentRef = await firestore.doc(db.TEACHER.STUDENTS.DOC(teacherId, code));
|
|
540
|
+
const assessments = await firestore
|
|
541
|
+
.collection(db.ASSESSMENT.COLLECTION())
|
|
542
|
+
.get();
|
|
543
|
+
for (const assessment of assessments.docs) {
|
|
544
|
+
const assessmentResultRef = await firestore.doc(db.TEACHER.ASSESSMENT_RESULTS.DOC(teacherId, assessment.id, code));
|
|
545
|
+
await assessmentResultRef.delete();
|
|
546
|
+
const assessmentReportRef = await firestore.doc(db.TEACHER.ASSESSMENT_REPORTS.DOC(teacherId, assessment.id, code));
|
|
547
|
+
await assessmentReportRef.delete();
|
|
548
|
+
}
|
|
549
|
+
await studentRef.delete();
|
|
550
|
+
};
|
|
551
|
+
export const getUser = async (uid) => {
|
|
552
|
+
const firestore = getFirestore();
|
|
553
|
+
const userFromDatabase = (await firestore.doc(new DATABASE().AUTH.DOC(uid)).get()).data();
|
|
554
|
+
return userFromDatabase;
|
|
555
|
+
};
|
|
556
|
+
export const setUser = async (uid, user) => {
|
|
557
|
+
const firestore = getFirestore();
|
|
558
|
+
await firestore.doc(new DATABASE().AUTH.DOC(uid)).set(user);
|
|
559
|
+
};
|
|
560
|
+
export const resetSession = async (db, teacherId, code, assessmentId) => {
|
|
561
|
+
const firestore = getFirestore();
|
|
562
|
+
const studentRef = await firestore
|
|
563
|
+
.doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
|
|
564
|
+
.get();
|
|
565
|
+
const student = studentRef.data();
|
|
566
|
+
const updatedStudent = {
|
|
567
|
+
...student,
|
|
568
|
+
sessions: student.sessions.map((s) => s.assessmentId === assessmentId
|
|
569
|
+
? { ...s, sessionState: "not_started" }
|
|
570
|
+
: s),
|
|
571
|
+
};
|
|
572
|
+
await studentRef.ref.set(updatedStudent);
|
|
573
|
+
};
|
|
574
|
+
export const getStudentInfoForTeacher = async (db, teacherId, code) => {
|
|
575
|
+
const firestore = getFirestore();
|
|
576
|
+
const studentRef = await firestore
|
|
577
|
+
.doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
|
|
578
|
+
.get();
|
|
579
|
+
const student = studentRef.data();
|
|
580
|
+
return student;
|
|
581
|
+
};
|
|
582
|
+
export const updateStudent = async (db, teacherId, code, identification) => {
|
|
583
|
+
const firestore = getFirestore();
|
|
584
|
+
const studentRef = await firestore
|
|
585
|
+
.doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
|
|
586
|
+
.get();
|
|
587
|
+
const student = studentRef.data();
|
|
588
|
+
const updatedStudent = {
|
|
589
|
+
...student,
|
|
590
|
+
identification,
|
|
591
|
+
};
|
|
592
|
+
await studentRef.ref.set(updatedStudent);
|
|
593
|
+
};
|
|
594
|
+
export const getAllApplications = async () => {
|
|
595
|
+
const firestore = getFirestore();
|
|
596
|
+
const applicationCollection = await firestore
|
|
597
|
+
.collection(new DATABASE().APPLICATION.COLLECTION)
|
|
598
|
+
.get();
|
|
599
|
+
const applications = applicationCollection.docs.map((doc) => {
|
|
600
|
+
const app = doc.data();
|
|
601
|
+
if (!app.name) {
|
|
602
|
+
app.name = doc.id;
|
|
603
|
+
}
|
|
604
|
+
return app;
|
|
605
|
+
});
|
|
606
|
+
return applications;
|
|
607
|
+
};
|
|
608
|
+
const createStrongPassword = () => {
|
|
609
|
+
const password = Math.random().toString(36).slice(-8);
|
|
610
|
+
return password;
|
|
611
|
+
};
|
|
612
|
+
export const createUserForApplication = async (email, applicationId) => {
|
|
613
|
+
const auth = getAuth();
|
|
614
|
+
const firestore = getFirestore();
|
|
615
|
+
let password = "";
|
|
616
|
+
let firebaseUser = undefined;
|
|
617
|
+
try {
|
|
618
|
+
firebaseUser = await auth.getUserByEmail(email);
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
// if the user is not found, then it raises an error.
|
|
622
|
+
}
|
|
623
|
+
if (!firebaseUser) {
|
|
624
|
+
password = createStrongPassword();
|
|
625
|
+
firebaseUser = await auth.createUser({
|
|
626
|
+
email,
|
|
627
|
+
password,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
await firestore
|
|
631
|
+
.doc(new DATABASE().AUTH.CHECK_APP_RIGHTS(firebaseUser.uid, applicationId))
|
|
632
|
+
.set({
|
|
633
|
+
email,
|
|
634
|
+
application: applicationId.toLocaleLowerCase(),
|
|
635
|
+
userId: firebaseUser.uid,
|
|
636
|
+
});
|
|
637
|
+
if (password) {
|
|
638
|
+
return { password, id: firebaseUser.uid };
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
return { id: firebaseUser.uid };
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
export const deleteUserForApplication = async (id, applicationId) => {
|
|
645
|
+
const auth = getAuth();
|
|
646
|
+
const firestore = getFirestore();
|
|
647
|
+
// check application right. If the user has rights to just this application then the user can be deleted.
|
|
648
|
+
// otherwise we remove the user from firebase Authentication
|
|
649
|
+
const applicationDocs = await firestore
|
|
650
|
+
.collection(new DATABASE().AUTH.APP_COLLECTION(id))
|
|
651
|
+
.listDocuments();
|
|
652
|
+
const applicationIds = applicationDocs.map((a) => a.id.toLocaleLowerCase());
|
|
653
|
+
if (!applicationIds.includes(applicationId.toLocaleLowerCase())) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
await firestore
|
|
657
|
+
.doc(new DATABASE().AUTH.CHECK_APP_RIGHTS(id, applicationId))
|
|
658
|
+
.delete();
|
|
659
|
+
if (applicationIds.length === 1) {
|
|
660
|
+
await auth.deleteUser(id);
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
};
|
|
664
|
+
export const getAllApplicationsWithUsers = async () => {
|
|
665
|
+
const firestore = getFirestore();
|
|
666
|
+
const applications = await getAllApplications();
|
|
667
|
+
const userDocs = await firestore
|
|
668
|
+
.collection(new DATABASE().AUTH.COLLECTION)
|
|
669
|
+
.listDocuments();
|
|
670
|
+
const usersPerApplication = new Map();
|
|
671
|
+
const allUserIds = userDocs.map((u) => u.id);
|
|
672
|
+
const users = await fetchUsersInChunks(allUserIds);
|
|
673
|
+
for (const userId of allUserIds) {
|
|
674
|
+
const applicationsForUser = await firestore
|
|
675
|
+
.collection(new DATABASE().AUTH.APP_COLLECTION(userId))
|
|
676
|
+
.listDocuments();
|
|
677
|
+
for (const applicationId of applicationsForUser.map((a) => a.id.toLowerCase())) {
|
|
678
|
+
const userData = users.find((u) => u.id === userId);
|
|
679
|
+
const user = {
|
|
680
|
+
id: userId,
|
|
681
|
+
email: userData?.email || "",
|
|
682
|
+
disabled: userData?.disabled || false,
|
|
683
|
+
};
|
|
684
|
+
if (usersPerApplication.has(applicationId)) {
|
|
685
|
+
usersPerApplication.get(applicationId)?.push(user);
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
usersPerApplication.set(applicationId, [user]);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const applicationWithUsers = applications.map((a) => {
|
|
693
|
+
return {
|
|
694
|
+
...a,
|
|
695
|
+
users: usersPerApplication.get(a.name?.toLocaleLowerCase()) || [],
|
|
696
|
+
};
|
|
697
|
+
});
|
|
698
|
+
return applicationWithUsers;
|
|
699
|
+
};
|
|
700
|
+
export async function deleteDocumentAndSubcollections(docRef) {
|
|
701
|
+
// Delete the document
|
|
702
|
+
await docRef.delete();
|
|
703
|
+
// Get all the subcollections of the document
|
|
704
|
+
const collections = await docRef.listCollections();
|
|
705
|
+
// Delete each subcollection recursively
|
|
706
|
+
for (const collection of collections) {
|
|
707
|
+
const querySnapshot = await collection.listDocuments();
|
|
708
|
+
querySnapshot.forEach(async (doc) => {
|
|
709
|
+
// Delete the subdocument and its subcollections recursively
|
|
710
|
+
await deleteDocumentAndSubcollections(doc);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
export async function checkAndDeletePackages(dateTime) {
|
|
715
|
+
const firestore = getFirestore();
|
|
716
|
+
const storage = getStorage();
|
|
717
|
+
const db = new DATABASE();
|
|
718
|
+
const packageData = await firestore.collection(db.PACKAGE.COLLECTION()).get();
|
|
719
|
+
const packages = packageData.docs.map((doc) => doc.data());
|
|
720
|
+
for (const packageInfo of packages) {
|
|
721
|
+
if (packageInfo.uploaded < dateTime) {
|
|
722
|
+
for (const { assessmentId } of packageInfo.assessmentInfo) {
|
|
723
|
+
await deleteDocumentAndSubcollections(firestore.doc(db.ASSESSMENT.DOC(assessmentId)));
|
|
724
|
+
}
|
|
725
|
+
await deleteFilesRecursive(storage.bucket(), `packages/${packageInfo.packageId}`);
|
|
726
|
+
await deleteDocumentAndSubcollections(firestore.doc(db.PACKAGE.DOC(packageInfo.packageId)));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
export async function fetchUsersInChunks(ids, chunkSize = 100) {
|
|
731
|
+
let results = [];
|
|
732
|
+
const auth = getAuth();
|
|
733
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
734
|
+
const chunk = ids.slice(i, i + chunkSize);
|
|
735
|
+
try {
|
|
736
|
+
const chunkResults = await auth.getUsers(chunk.map((c) => ({ uid: c })));
|
|
737
|
+
results = results.concat(chunkResults.users.map((u) => {
|
|
738
|
+
return {
|
|
739
|
+
id: u.uid || "",
|
|
740
|
+
disabled: u.disabled,
|
|
741
|
+
email: u.email || "",
|
|
742
|
+
};
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
console.error("Error fetching users:", error);
|
|
747
|
+
// Handle error appropriately - possibly rethrow, or continue to next chunk
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return results;
|
|
751
|
+
}
|
|
752
|
+
export const getPackageIdByAssessment = async (db, code, assessmentId) => {
|
|
753
|
+
const fs = getFirestore();
|
|
754
|
+
const docRef = await fs.doc(db.STUDENT.DOC(code)).get();
|
|
755
|
+
const studentInfo = docRef.data();
|
|
756
|
+
const session = studentInfo.sessions.find((s) => s.assessmentId === assessmentId);
|
|
757
|
+
return session?.packageId || null;
|
|
758
|
+
};
|
|
759
|
+
//# sourceMappingURL=logic.js.map
|