@vue-skuilder/db 0.1.6 → 0.1.8-0
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/{SyncStrategy-DnJRj-Xp.d.mts → SyncStrategy-CyATpyLQ.d.mts} +6 -0
- package/dist/{SyncStrategy-DnJRj-Xp.d.ts → SyncStrategy-CyATpyLQ.d.ts} +6 -0
- package/dist/core/index.d.mts +5 -5
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +825 -762
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +812 -750
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BZmLyBVw.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
- package/dist/{dataLayerProvider-BuntXkCs.d.ts → dataLayerProvider-DqtNroSh.d.ts} +1 -1
- package/dist/impl/couch/index.d.mts +6 -6
- package/dist/impl/couch/index.d.ts +6 -6
- package/dist/impl/couch/index.js +2261 -2081
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2274 -2095
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +8 -6
- package/dist/impl/static/index.d.ts +8 -6
- package/dist/impl/static/index.js +524 -1064
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +515 -1058
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index-CLL31bEy.d.ts +137 -0
- package/dist/index-CUNnL38E.d.mts +137 -0
- package/dist/index.d.mts +200 -9
- package/dist/index.d.ts +200 -9
- package/dist/index.js +4123 -2820
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4119 -2830
- package/dist/index.mjs.map +1 -1
- package/dist/{types-D6SnlHPm.d.ts → types-BefDGkKa.d.ts} +1 -1
- package/dist/{types-DPRvCrIk.d.mts → types-DC-ckZug.d.mts} +1 -1
- package/dist/{types-legacy-WPe8CtO-.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
- package/dist/{types-legacy-WPe8CtO-.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
- package/dist/{userDB-D9EuWTp1.d.ts → userDB-C33Hzjgn.d.mts} +11 -4
- package/dist/{userDB-31gsvxyd.d.mts → userDB-DusL7OXe.d.ts} +11 -4
- package/dist/util/packer/index.d.mts +3 -63
- package/dist/util/packer/index.d.ts +3 -63
- package/dist/util/packer/index.js +53 -1
- package/dist/util/packer/index.js.map +1 -1
- package/dist/util/packer/index.mjs +53 -1
- package/dist/util/packer/index.mjs.map +1 -1
- package/package.json +7 -4
- package/src/core/types/types-legacy.ts +13 -1
- package/src/core/types/user.ts +9 -2
- package/src/core/util/index.ts +5 -4
- package/src/factory.ts +25 -0
- package/src/impl/common/BaseUserDB.ts +62 -28
- package/src/impl/common/SyncStrategy.ts +7 -0
- package/src/impl/common/index.ts +0 -1
- package/src/impl/common/userDBHelpers.ts +15 -5
- package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
- package/src/impl/couch/courseAPI.ts +7 -6
- package/src/impl/couch/courseLookupDB.ts +24 -0
- package/src/impl/couch/index.ts +10 -5
- package/src/impl/couch/updateQueue.ts +12 -8
- package/src/impl/couch/user-course-relDB.ts +17 -27
- package/src/impl/static/NoOpSyncStrategy.ts +5 -0
- package/src/impl/static/StaticDataUnpacker.ts +18 -36
- package/src/impl/static/courseDB.ts +135 -17
- package/src/util/dataDirectory.test.ts +53 -0
- package/src/util/dataDirectory.ts +52 -0
- package/src/util/index.ts +3 -0
- package/src/util/migrator/FileSystemAdapter.ts +79 -0
- package/src/util/migrator/StaticToCouchDBMigrator.ts +713 -0
- package/src/util/migrator/index.ts +18 -0
- package/src/util/migrator/types.ts +84 -0
- package/src/util/migrator/validation.ts +517 -0
- package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
- package/src/util/tuiLogger.ts +139 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __glob = (map) => (
|
|
4
|
-
var fn = map[
|
|
3
|
+
var __glob = (map) => (path2) => {
|
|
4
|
+
var fn = map[path2];
|
|
5
5
|
if (fn) return fn();
|
|
6
|
-
throw new Error("Module not found in bundle: " +
|
|
6
|
+
throw new Error("Module not found in bundle: " + path2);
|
|
7
7
|
};
|
|
8
8
|
var __esm = (fn, res) => function __init() {
|
|
9
9
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
@@ -13,6 +13,27 @@ var __export = (target, all) => {
|
|
|
13
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
// src/impl/common/SyncStrategy.ts
|
|
17
|
+
var init_SyncStrategy = __esm({
|
|
18
|
+
"src/impl/common/SyncStrategy.ts"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// src/core/interfaces/adminDB.ts
|
|
24
|
+
var init_adminDB = __esm({
|
|
25
|
+
"src/core/interfaces/adminDB.ts"() {
|
|
26
|
+
"use strict";
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// src/core/interfaces/classroomDB.ts
|
|
31
|
+
var init_classroomDB = __esm({
|
|
32
|
+
"src/core/interfaces/classroomDB.ts"() {
|
|
33
|
+
"use strict";
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
16
37
|
// src/util/logger.ts
|
|
17
38
|
var isDevelopment, logger;
|
|
18
39
|
var init_logger = __esm({
|
|
@@ -58,40 +79,6 @@ var init_logger = __esm({
|
|
|
58
79
|
}
|
|
59
80
|
});
|
|
60
81
|
|
|
61
|
-
// src/factory.ts
|
|
62
|
-
function getDataLayer() {
|
|
63
|
-
if (!dataLayerInstance) {
|
|
64
|
-
throw new Error("Data layer not initialized. Call initializeDataLayer first.");
|
|
65
|
-
}
|
|
66
|
-
return dataLayerInstance;
|
|
67
|
-
}
|
|
68
|
-
var ENV, dataLayerInstance;
|
|
69
|
-
var init_factory = __esm({
|
|
70
|
-
"src/factory.ts"() {
|
|
71
|
-
"use strict";
|
|
72
|
-
init_logger();
|
|
73
|
-
ENV = {
|
|
74
|
-
COUCHDB_SERVER_PROTOCOL: "NOT_SET",
|
|
75
|
-
COUCHDB_SERVER_URL: "NOT_SET"
|
|
76
|
-
};
|
|
77
|
-
dataLayerInstance = null;
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// src/core/types/types-legacy.ts
|
|
82
|
-
var GuestUsername, log, cardHistoryPrefix;
|
|
83
|
-
var init_types_legacy = __esm({
|
|
84
|
-
"src/core/types/types-legacy.ts"() {
|
|
85
|
-
"use strict";
|
|
86
|
-
init_logger();
|
|
87
|
-
GuestUsername = "Guest";
|
|
88
|
-
log = (message) => {
|
|
89
|
-
logger.log(message);
|
|
90
|
-
};
|
|
91
|
-
cardHistoryPrefix = "cardH";
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
82
|
// src/impl/couch/pouchdb-setup.ts
|
|
96
83
|
import PouchDB from "pouchdb";
|
|
97
84
|
import PouchDBFind from "pouchdb-find";
|
|
@@ -138,7 +125,10 @@ var init_updateQueue = __esm({
|
|
|
138
125
|
_className = "UpdateQueue";
|
|
139
126
|
pendingUpdates = {};
|
|
140
127
|
inprogressUpdates = {};
|
|
141
|
-
|
|
128
|
+
readDB;
|
|
129
|
+
// Database for read operations
|
|
130
|
+
writeDB;
|
|
131
|
+
// Database for write operations (local-first)
|
|
142
132
|
update(id, update) {
|
|
143
133
|
logger.debug(`Update requested on doc: ${id}`);
|
|
144
134
|
if (this.pendingUpdates[id]) {
|
|
@@ -148,24 +138,25 @@ var init_updateQueue = __esm({
|
|
|
148
138
|
}
|
|
149
139
|
return this.applyUpdates(id);
|
|
150
140
|
}
|
|
151
|
-
constructor(
|
|
141
|
+
constructor(readDB, writeDB) {
|
|
152
142
|
super();
|
|
153
|
-
this.
|
|
143
|
+
this.readDB = readDB;
|
|
144
|
+
this.writeDB = writeDB || readDB;
|
|
154
145
|
logger.debug(`UpdateQ initialized...`);
|
|
155
|
-
void this.
|
|
146
|
+
void this.readDB.info().then((i) => {
|
|
156
147
|
logger.debug(`db info: ${JSON.stringify(i)}`);
|
|
157
148
|
});
|
|
158
149
|
}
|
|
159
150
|
async applyUpdates(id) {
|
|
160
151
|
logger.debug(`Applying updates on doc: ${id}`);
|
|
161
152
|
if (this.inprogressUpdates[id]) {
|
|
162
|
-
await this.
|
|
153
|
+
await this.readDB.info();
|
|
163
154
|
return this.applyUpdates(id);
|
|
164
155
|
} else {
|
|
165
156
|
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
166
157
|
this.inprogressUpdates[id] = true;
|
|
167
158
|
try {
|
|
168
|
-
let doc = await this.
|
|
159
|
+
let doc = await this.readDB.get(id);
|
|
169
160
|
logger.debug(`Retrieved doc: ${id}`);
|
|
170
161
|
while (this.pendingUpdates[id].length !== 0) {
|
|
171
162
|
const update = this.pendingUpdates[id].splice(0, 1)[0];
|
|
@@ -178,7 +169,7 @@ var init_updateQueue = __esm({
|
|
|
178
169
|
};
|
|
179
170
|
}
|
|
180
171
|
}
|
|
181
|
-
await this.
|
|
172
|
+
await this.writeDB.put(doc);
|
|
182
173
|
logger.debug(`Put doc: ${id}`);
|
|
183
174
|
if (this.pendingUpdates[id].length === 0) {
|
|
184
175
|
this.inprogressUpdates[id] = false;
|
|
@@ -204,6 +195,32 @@ var init_updateQueue = __esm({
|
|
|
204
195
|
}
|
|
205
196
|
});
|
|
206
197
|
|
|
198
|
+
// src/core/types/types-legacy.ts
|
|
199
|
+
var GuestUsername, log, DocTypePrefixes;
|
|
200
|
+
var init_types_legacy = __esm({
|
|
201
|
+
"src/core/types/types-legacy.ts"() {
|
|
202
|
+
"use strict";
|
|
203
|
+
init_logger();
|
|
204
|
+
GuestUsername = "Guest";
|
|
205
|
+
log = (message) => {
|
|
206
|
+
logger.log(message);
|
|
207
|
+
};
|
|
208
|
+
DocTypePrefixes = {
|
|
209
|
+
["CARD" /* CARD */]: "c",
|
|
210
|
+
["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]: "dd",
|
|
211
|
+
["TAG" /* TAG */]: "TAG",
|
|
212
|
+
["CARDRECORD" /* CARDRECORD */]: "cardH",
|
|
213
|
+
["SCHEDULED_CARD" /* SCHEDULED_CARD */]: "card_review_",
|
|
214
|
+
// Add other doctypes here as they get prefixed IDs
|
|
215
|
+
["DATASHAPE" /* DATASHAPE */]: "DATASHAPE",
|
|
216
|
+
["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
|
|
217
|
+
["VIEW" /* VIEW */]: "VIEW",
|
|
218
|
+
["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
|
|
219
|
+
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
207
224
|
// src/impl/couch/clientCache.ts
|
|
208
225
|
async function GET_CACHED(k, f) {
|
|
209
226
|
if (CLIENT_CACHE[k]) {
|
|
@@ -223,2293 +240,2450 @@ var init_clientCache = __esm({
|
|
|
223
240
|
}
|
|
224
241
|
});
|
|
225
242
|
|
|
226
|
-
// src/impl/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
// src/impl/couch/courseAPI.ts
|
|
244
|
+
import { NameSpacer } from "@vue-skuilder/common";
|
|
245
|
+
import { blankCourseElo, toCourseElo } from "@vue-skuilder/common";
|
|
246
|
+
import { prepareNote55 } from "@vue-skuilder/common";
|
|
247
|
+
import { v4 as uuidv4 } from "uuid";
|
|
248
|
+
async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo()) {
|
|
249
|
+
const db = getCourseDB(courseID);
|
|
250
|
+
const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads);
|
|
251
|
+
const _id = `${DocTypePrefixes["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]}-${uuidv4()}`;
|
|
252
|
+
const result = await db.put({ ...payload, _id });
|
|
253
|
+
const dataShapeId = NameSpacer.getDataShapeString({
|
|
254
|
+
course: codeCourse,
|
|
255
|
+
dataShape: shape.name
|
|
256
|
+
});
|
|
257
|
+
if (result.ok) {
|
|
258
|
+
try {
|
|
259
|
+
await createCards(courseID, dataShapeId, result.id, tags, elo, author);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
let errorMessage = "Unknown error";
|
|
262
|
+
if (error instanceof Error) {
|
|
263
|
+
errorMessage = error.message;
|
|
264
|
+
} else if (error && typeof error === "object" && "reason" in error) {
|
|
265
|
+
errorMessage = error.reason;
|
|
266
|
+
} else if (error && typeof error === "object" && "message" in error) {
|
|
267
|
+
errorMessage = error.message;
|
|
268
|
+
} else {
|
|
269
|
+
errorMessage = String(error);
|
|
270
|
+
}
|
|
271
|
+
logger.error(`[addNote55] Failed to create cards for note ${result.id}: ${errorMessage}`);
|
|
272
|
+
result.cardCreationFailed = true;
|
|
273
|
+
result.cardCreationError = errorMessage;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
logger.error(`[addNote55] Error adding note. Result: ${JSON.stringify(result)}`);
|
|
230
277
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// src/core/util/index.ts
|
|
234
|
-
function getCardHistoryID(courseID, cardID) {
|
|
235
|
-
return `${cardHistoryPrefix}-${courseID}-${cardID}`;
|
|
278
|
+
return result;
|
|
236
279
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
280
|
+
async function createCards(courseID, datashapeID, noteID, tags, elo = blankCourseElo(), author) {
|
|
281
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
282
|
+
const dsDescriptor = NameSpacer.getDataShapeDescriptor(datashapeID);
|
|
283
|
+
let questionViewTypes = [];
|
|
284
|
+
for (const ds of cfg.dataShapes) {
|
|
285
|
+
if (ds.name === datashapeID) {
|
|
286
|
+
questionViewTypes = ds.questionTypes;
|
|
287
|
+
}
|
|
241
288
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
function hexEncode(str) {
|
|
247
|
-
let hex;
|
|
248
|
-
let returnStr = "";
|
|
249
|
-
for (let i = 0; i < str.length; i++) {
|
|
250
|
-
hex = str.charCodeAt(i).toString(16);
|
|
251
|
-
returnStr += ("000" + hex).slice(3);
|
|
289
|
+
if (questionViewTypes.length === 0) {
|
|
290
|
+
const errorMsg = `No questionViewTypes found for datashapeID: ${datashapeID} in course config. Cards cannot be created.`;
|
|
291
|
+
logger.error(errorMsg);
|
|
292
|
+
throw new Error(errorMsg);
|
|
252
293
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
function filterAllDocsByPrefix(db, prefix, opts) {
|
|
256
|
-
const options = {
|
|
257
|
-
startkey: prefix,
|
|
258
|
-
endkey: prefix + "\uFFF0",
|
|
259
|
-
include_docs: true
|
|
260
|
-
};
|
|
261
|
-
if (opts) {
|
|
262
|
-
Object.assign(options, opts);
|
|
294
|
+
for (const questionView of questionViewTypes) {
|
|
295
|
+
await createCard(questionView, courseID, dsDescriptor, noteID, tags, elo, author);
|
|
263
296
|
}
|
|
264
|
-
return db.allDocs(options);
|
|
265
297
|
}
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
298
|
+
async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags, elo = blankCourseElo(), author) {
|
|
299
|
+
const qDescriptor = NameSpacer.getQuestionDescriptor(questionViewName);
|
|
300
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
301
|
+
for (const rQ of cfg.questionTypes) {
|
|
302
|
+
if (rQ.name === questionViewName) {
|
|
303
|
+
for (const view of rQ.viewList) {
|
|
304
|
+
await addCard(
|
|
305
|
+
courseID,
|
|
306
|
+
dsDescriptor.course,
|
|
307
|
+
[noteID],
|
|
308
|
+
NameSpacer.getViewString({
|
|
309
|
+
course: qDescriptor.course,
|
|
310
|
+
questionType: qDescriptor.questionType,
|
|
311
|
+
view
|
|
312
|
+
}),
|
|
313
|
+
elo,
|
|
314
|
+
tags,
|
|
315
|
+
author
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
271
320
|
}
|
|
272
|
-
function
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return guestDB.put({
|
|
284
|
-
_id: expiryDocID2,
|
|
285
|
-
date: expirationDate
|
|
286
|
-
});
|
|
321
|
+
async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
|
|
322
|
+
const db = getCourseDB(courseID);
|
|
323
|
+
const _id = `${DocTypePrefixes["CARD" /* CARD */]}-${uuidv4()}`;
|
|
324
|
+
const card = await db.put({
|
|
325
|
+
_id,
|
|
326
|
+
course,
|
|
327
|
+
id_displayable_data,
|
|
328
|
+
id_view,
|
|
329
|
+
docType: "CARD" /* CARD */,
|
|
330
|
+
elo: elo || toCourseElo(990 + Math.round(20 * Math.random())),
|
|
331
|
+
author
|
|
287
332
|
});
|
|
333
|
+
for (const tag of tags) {
|
|
334
|
+
logger.info(`adding tag: ${tag} to card ${card.id}`);
|
|
335
|
+
await addTagToCard(courseID, card.id, tag, author, false);
|
|
336
|
+
}
|
|
337
|
+
return card;
|
|
288
338
|
}
|
|
289
|
-
function
|
|
290
|
-
|
|
339
|
+
async function getCredentialledCourseConfig(courseID) {
|
|
340
|
+
try {
|
|
341
|
+
const db = getCourseDB(courseID);
|
|
342
|
+
const ret = await db.get("CourseConfig");
|
|
343
|
+
ret.courseID = courseID;
|
|
344
|
+
logger.info(`Returning course config: ${JSON.stringify(ret)}`);
|
|
345
|
+
return ret;
|
|
346
|
+
} catch (e) {
|
|
347
|
+
logger.error(`Error fetching config for ${courseID}:`, e);
|
|
348
|
+
throw e;
|
|
349
|
+
}
|
|
291
350
|
}
|
|
292
|
-
function
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
351
|
+
async function addTagToCard(courseID, cardID, tagID, author, updateELO = true) {
|
|
352
|
+
const prefixedTagID = getTagID(tagID);
|
|
353
|
+
const courseDB = getCourseDB(courseID);
|
|
354
|
+
const courseApi = new CourseDB(courseID, async () => {
|
|
355
|
+
const dummySyncStrategy = {
|
|
356
|
+
setupRemoteDB: () => null,
|
|
357
|
+
startSync: () => {
|
|
358
|
+
},
|
|
359
|
+
canCreateAccount: () => false,
|
|
360
|
+
canAuthenticate: () => false,
|
|
361
|
+
getCurrentUsername: async () => "DummyUser"
|
|
362
|
+
};
|
|
363
|
+
return BaseUser.Dummy(dummySyncStrategy);
|
|
303
364
|
});
|
|
365
|
+
try {
|
|
366
|
+
logger.info(`Applying tag ${tagID} to card ${courseID + "-" + cardID}...`);
|
|
367
|
+
const tag = await courseDB.get(prefixedTagID);
|
|
368
|
+
if (!tag.taggedCards.includes(cardID)) {
|
|
369
|
+
tag.taggedCards.push(cardID);
|
|
370
|
+
if (updateELO) {
|
|
371
|
+
try {
|
|
372
|
+
const eloData = await courseApi.getCardEloData([cardID]);
|
|
373
|
+
const elo = eloData[0];
|
|
374
|
+
elo.tags[tagID] = {
|
|
375
|
+
count: 0,
|
|
376
|
+
score: elo.global.score
|
|
377
|
+
// todo: or 1000?
|
|
378
|
+
};
|
|
379
|
+
await updateCardElo(courseID, cardID, elo);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
logger.error("Failed to update ELO data for card:", cardID, error);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return courseDB.put(tag);
|
|
385
|
+
} else throw new AlreadyTaggedErr(`Card ${cardID} is already tagged with ${tagID}`);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
if (e instanceof AlreadyTaggedErr) {
|
|
388
|
+
throw e;
|
|
389
|
+
}
|
|
390
|
+
await createTag(courseID, tagID, author);
|
|
391
|
+
return addTagToCard(courseID, cardID, tagID, author, updateELO);
|
|
392
|
+
}
|
|
304
393
|
}
|
|
305
|
-
async function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
${JSON.stringify(err)}`);
|
|
314
|
-
});
|
|
394
|
+
async function updateCardElo(courseID, cardID, elo) {
|
|
395
|
+
if (elo) {
|
|
396
|
+
const cDB = getCourseDB(courseID);
|
|
397
|
+
const card = await cDB.get(cardID);
|
|
398
|
+
logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
|
|
399
|
+
card.elo = elo;
|
|
400
|
+
return cDB.put(card);
|
|
401
|
+
}
|
|
315
402
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
403
|
+
function getTagID(tagName) {
|
|
404
|
+
const tagPrefix = "TAG" /* TAG */.valueOf() + "-";
|
|
405
|
+
if (tagName.indexOf(tagPrefix) === 0) {
|
|
406
|
+
return tagName;
|
|
407
|
+
} else {
|
|
408
|
+
return tagPrefix + tagName;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function getCourseDB(courseID) {
|
|
412
|
+
const dbName = `coursedb-${courseID}`;
|
|
413
|
+
return new pouchdb_setup_default(
|
|
414
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
415
|
+
pouchDBincludeCredentialsConfig
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
var AlreadyTaggedErr;
|
|
419
|
+
var init_courseAPI = __esm({
|
|
420
|
+
"src/impl/couch/courseAPI.ts"() {
|
|
319
421
|
"use strict";
|
|
320
|
-
init_logger();
|
|
321
422
|
init_pouchdb_setup();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
423
|
+
init_couch();
|
|
424
|
+
init_factory();
|
|
425
|
+
init_courseDB();
|
|
426
|
+
init_types_legacy();
|
|
427
|
+
init_common();
|
|
428
|
+
init_logger();
|
|
429
|
+
AlreadyTaggedErr = class extends Error {
|
|
430
|
+
constructor(message) {
|
|
431
|
+
super(message);
|
|
432
|
+
this.name = "AlreadyTaggedErr";
|
|
433
|
+
}
|
|
326
434
|
};
|
|
327
435
|
}
|
|
328
436
|
});
|
|
329
437
|
|
|
330
|
-
// src/impl/couch/
|
|
331
|
-
|
|
332
|
-
var
|
|
333
|
-
|
|
334
|
-
"src/impl/couch/user-course-relDB.ts"() {
|
|
438
|
+
// src/impl/couch/courseLookupDB.ts
|
|
439
|
+
var courseLookupDBTitle, CourseLookup;
|
|
440
|
+
var init_courseLookupDB = __esm({
|
|
441
|
+
"src/impl/couch/courseLookupDB.ts"() {
|
|
335
442
|
"use strict";
|
|
336
|
-
|
|
337
|
-
|
|
443
|
+
init_pouchdb_setup();
|
|
444
|
+
init_factory();
|
|
338
445
|
init_logger();
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
446
|
+
courseLookupDBTitle = "coursedb-lookup";
|
|
447
|
+
logger.debug(`COURSELOOKUP FILE RUNNING`);
|
|
448
|
+
CourseLookup = class _CourseLookup {
|
|
449
|
+
// [ ] this db should be read only for public, admin-only for write
|
|
450
|
+
// Cache for the PouchDB instance
|
|
451
|
+
static _dbInstance = null;
|
|
452
|
+
/**
|
|
453
|
+
* Static getter for the PouchDB database instance.
|
|
454
|
+
* Connects using ENV variables and caches the instance.
|
|
455
|
+
* Throws an error if required ENV variables are not set.
|
|
456
|
+
*/
|
|
457
|
+
static get _db() {
|
|
458
|
+
if (this._dbInstance) {
|
|
459
|
+
return this._dbInstance;
|
|
460
|
+
}
|
|
461
|
+
if (ENV.COUCHDB_SERVER_URL === "NOT_SET" || !ENV.COUCHDB_SERVER_URL) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
"CourseLookup.db: COUCHDB_SERVER_URL is not set. Ensure initializeDataLayer has been called with valid configuration."
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (ENV.COUCHDB_SERVER_PROTOCOL === "NOT_SET" || !ENV.COUCHDB_SERVER_PROTOCOL) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
"CourseLookup.db: COUCHDB_SERVER_PROTOCOL is not set. Ensure initializeDataLayer has been called with valid configuration."
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
const dbUrl = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}/${courseLookupDBTitle}`;
|
|
472
|
+
const options = {
|
|
473
|
+
skip_setup: true
|
|
474
|
+
// Keep the original option
|
|
475
|
+
// fetch: (url, opts) => { // Optional: Add for debugging network requests
|
|
476
|
+
// console.log('PouchDB fetch:', url, opts);
|
|
477
|
+
// return pouch.fetch(url, opts);
|
|
478
|
+
// }
|
|
479
|
+
};
|
|
480
|
+
if (ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD) {
|
|
481
|
+
options.auth = {
|
|
482
|
+
username: ENV.COUCHDB_USERNAME,
|
|
483
|
+
password: ENV.COUCHDB_PASSWORD
|
|
484
|
+
};
|
|
485
|
+
logger.info(`CourseLookup: Connecting to ${dbUrl} with authentication.`);
|
|
486
|
+
} else {
|
|
487
|
+
logger.info(`CourseLookup: Connecting to ${dbUrl} without authentication.`);
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
this._dbInstance = new pouchdb_setup_default(dbUrl, options);
|
|
491
|
+
logger.info(`CourseLookup: Database instance created for ${courseLookupDBTitle}.`);
|
|
492
|
+
return this._dbInstance;
|
|
493
|
+
} catch (error) {
|
|
494
|
+
logger.error(`CourseLookup: Failed to create PouchDB instance for ${dbUrl}`, error);
|
|
495
|
+
this._dbInstance = null;
|
|
496
|
+
throw new Error(
|
|
497
|
+
`CourseLookup: Failed to initialize database connection: ${error instanceof Error ? error.message : String(error)}`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
347
500
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Adds a new course to the lookup database, and returns the courseID
|
|
503
|
+
* @param courseName
|
|
504
|
+
* @returns
|
|
505
|
+
*/
|
|
506
|
+
static async add(courseName) {
|
|
507
|
+
const resp = await _CourseLookup._db.post({
|
|
508
|
+
name: courseName
|
|
509
|
+
});
|
|
510
|
+
return resp.id;
|
|
351
511
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
512
|
+
/**
|
|
513
|
+
* Adds a new course to the lookup database with a specific courseID
|
|
514
|
+
* @param courseId The specific course ID to use
|
|
515
|
+
* @param courseName The course name
|
|
516
|
+
* @param disambiguator Optional disambiguator
|
|
517
|
+
* @returns Promise<void>
|
|
518
|
+
*/
|
|
519
|
+
static async addWithId(courseId, courseName, disambiguator) {
|
|
520
|
+
const doc = {
|
|
521
|
+
_id: courseId,
|
|
522
|
+
name: courseName
|
|
523
|
+
};
|
|
524
|
+
if (disambiguator) {
|
|
525
|
+
doc.disambiguator = disambiguator;
|
|
526
|
+
}
|
|
527
|
+
await _CourseLookup._db.put(doc);
|
|
355
528
|
}
|
|
356
|
-
|
|
357
|
-
|
|
529
|
+
/**
|
|
530
|
+
* Removes a course from the index
|
|
531
|
+
* @param courseID
|
|
532
|
+
*/
|
|
533
|
+
static async delete(courseID) {
|
|
534
|
+
const doc = await _CourseLookup._db.get(courseID);
|
|
535
|
+
return await _CourseLookup._db.remove(doc);
|
|
358
536
|
}
|
|
359
|
-
async
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
537
|
+
static async allCourses() {
|
|
538
|
+
const resp = await _CourseLookup._db.allDocs({
|
|
539
|
+
include_docs: true
|
|
540
|
+
});
|
|
541
|
+
return resp.rows.map((row) => row.doc);
|
|
542
|
+
}
|
|
543
|
+
static async updateDisambiguator(courseID, disambiguator) {
|
|
544
|
+
const doc = await _CourseLookup._db.get(courseID);
|
|
545
|
+
doc.disambiguator = disambiguator;
|
|
546
|
+
return await _CourseLookup._db.put(doc);
|
|
547
|
+
}
|
|
548
|
+
static async isCourse(courseID) {
|
|
549
|
+
try {
|
|
550
|
+
await _CourseLookup._db.get(courseID);
|
|
551
|
+
return true;
|
|
552
|
+
} catch (error) {
|
|
553
|
+
logger.info(`Courselookup failed:`, error);
|
|
554
|
+
return false;
|
|
367
555
|
}
|
|
368
556
|
}
|
|
369
|
-
|
|
370
|
-
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// src/core/navigators/elo.ts
|
|
562
|
+
var elo_exports = {};
|
|
563
|
+
__export(elo_exports, {
|
|
564
|
+
default: () => ELONavigator
|
|
565
|
+
});
|
|
566
|
+
var ELONavigator;
|
|
567
|
+
var init_elo = __esm({
|
|
568
|
+
"src/core/navigators/elo.ts"() {
|
|
569
|
+
"use strict";
|
|
570
|
+
init_navigators();
|
|
571
|
+
ELONavigator = class extends ContentNavigator {
|
|
572
|
+
user;
|
|
573
|
+
course;
|
|
574
|
+
constructor(user, course) {
|
|
575
|
+
super();
|
|
576
|
+
this.user = user;
|
|
577
|
+
this.course = course;
|
|
371
578
|
}
|
|
372
|
-
async
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
579
|
+
async getPendingReviews() {
|
|
580
|
+
const reviews = await this.user.getPendingReviews(this.course.getCourseID());
|
|
581
|
+
const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
|
|
582
|
+
const ratedReviews = reviews.map((r, i) => {
|
|
583
|
+
const ratedR = {
|
|
584
|
+
...r,
|
|
585
|
+
...elo[i]
|
|
586
|
+
};
|
|
587
|
+
return ratedR;
|
|
378
588
|
});
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
);
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
589
|
+
ratedReviews.sort((a, b) => {
|
|
590
|
+
return a.global.score - b.global.score;
|
|
591
|
+
});
|
|
592
|
+
return ratedReviews.map((r) => {
|
|
593
|
+
return {
|
|
594
|
+
...r,
|
|
595
|
+
contentSourceType: "course",
|
|
596
|
+
contentSourceID: this.course.getCourseID(),
|
|
597
|
+
cardID: r.cardId,
|
|
598
|
+
courseID: r.courseId,
|
|
599
|
+
qualifiedID: `${r.courseId}-${r.cardId}`,
|
|
600
|
+
reviewID: r._id,
|
|
601
|
+
status: "review"
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
async getNewCards(limit = 99) {
|
|
606
|
+
const activeCards = await this.user.getActiveCards();
|
|
607
|
+
return (await this.course.getCardsCenteredAtELO({ limit, elo: "user" }, (c) => {
|
|
608
|
+
if (activeCards.some((ac) => c.includes(ac))) {
|
|
609
|
+
return false;
|
|
610
|
+
} else {
|
|
611
|
+
return true;
|
|
390
612
|
}
|
|
391
|
-
}).map((
|
|
613
|
+
})).map((c) => {
|
|
614
|
+
return {
|
|
615
|
+
...c,
|
|
616
|
+
status: "new"
|
|
617
|
+
};
|
|
618
|
+
});
|
|
392
619
|
}
|
|
393
620
|
};
|
|
394
621
|
}
|
|
395
622
|
});
|
|
396
623
|
|
|
397
|
-
// src/
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const err = e;
|
|
406
|
-
if (err.status === 404) {
|
|
407
|
-
await getLocalUserDB(user).put({
|
|
408
|
-
_id: userClassroomsDoc,
|
|
409
|
-
registrations: []
|
|
410
|
-
});
|
|
411
|
-
ret = await getOrCreateClassroomRegistrationsDoc(user);
|
|
412
|
-
} else {
|
|
413
|
-
const errorDetails = {
|
|
414
|
-
name: err.name,
|
|
415
|
-
status: err.status,
|
|
416
|
-
message: err.message,
|
|
417
|
-
reason: err.reason,
|
|
418
|
-
error: err.error
|
|
419
|
-
};
|
|
420
|
-
logger.error(
|
|
421
|
-
"Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
|
|
422
|
-
errorDetails
|
|
423
|
-
);
|
|
424
|
-
throw new Error(
|
|
425
|
-
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return ret;
|
|
430
|
-
}
|
|
431
|
-
async function getOrCreateCourseRegistrationsDoc(user) {
|
|
432
|
-
let ret;
|
|
433
|
-
try {
|
|
434
|
-
ret = await getLocalUserDB(user).get(userCoursesDoc);
|
|
435
|
-
} catch (e) {
|
|
436
|
-
const err = e;
|
|
437
|
-
if (err.status === 404) {
|
|
438
|
-
await getLocalUserDB(user).put({
|
|
439
|
-
_id: userCoursesDoc,
|
|
440
|
-
courses: [],
|
|
441
|
-
studyWeight: {}
|
|
442
|
-
});
|
|
443
|
-
ret = await getOrCreateCourseRegistrationsDoc(user);
|
|
444
|
-
} else {
|
|
445
|
-
throw new Error(
|
|
446
|
-
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
447
|
-
);
|
|
448
|
-
}
|
|
624
|
+
// import("./**/*") in src/core/navigators/index.ts
|
|
625
|
+
var globImport;
|
|
626
|
+
var init_ = __esm({
|
|
627
|
+
'import("./**/*") in src/core/navigators/index.ts'() {
|
|
628
|
+
globImport = __glob({
|
|
629
|
+
"./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
630
|
+
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
631
|
+
});
|
|
449
632
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const regItem = {
|
|
462
|
-
classID,
|
|
463
|
-
registeredAs: registerAs
|
|
464
|
-
};
|
|
465
|
-
if (doc.registrations.filter((reg) => {
|
|
466
|
-
return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
|
|
467
|
-
}).length === 0) {
|
|
468
|
-
doc.registrations.push(regItem);
|
|
469
|
-
} else {
|
|
470
|
-
log3(`User ${user} is already registered for class ${classID}`);
|
|
471
|
-
}
|
|
472
|
-
return getLocalUserDB(user).put(doc);
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
async function dropUserFromClassroom(user, classID) {
|
|
476
|
-
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
477
|
-
let index = -1;
|
|
478
|
-
for (let i = 0; i < doc.registrations.length; i++) {
|
|
479
|
-
if (doc.registrations[i].classID === classID) {
|
|
480
|
-
index = i;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
if (index !== -1) {
|
|
484
|
-
doc.registrations.splice(index, 1);
|
|
485
|
-
}
|
|
486
|
-
return getLocalUserDB(user).put(doc);
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
async function getUserClassrooms(user) {
|
|
490
|
-
return getOrCreateClassroomRegistrationsDoc(user);
|
|
491
|
-
}
|
|
492
|
-
var log3, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
493
|
-
var init_BaseUserDB = __esm({
|
|
494
|
-
"src/impl/common/BaseUserDB.ts"() {
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// src/core/navigators/index.ts
|
|
636
|
+
var navigators_exports = {};
|
|
637
|
+
__export(navigators_exports, {
|
|
638
|
+
ContentNavigator: () => ContentNavigator,
|
|
639
|
+
Navigators: () => Navigators
|
|
640
|
+
});
|
|
641
|
+
var Navigators, ContentNavigator;
|
|
642
|
+
var init_navigators = __esm({
|
|
643
|
+
"src/core/navigators/index.ts"() {
|
|
495
644
|
"use strict";
|
|
496
|
-
init_util();
|
|
497
|
-
init_types_legacy();
|
|
498
645
|
init_logger();
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
};
|
|
506
|
-
cardHistoryPrefix2 = "cardH-";
|
|
507
|
-
BaseUser = class _BaseUser {
|
|
508
|
-
static _instance;
|
|
509
|
-
static _initialized = false;
|
|
510
|
-
static Dummy(syncStrategy) {
|
|
511
|
-
return new _BaseUser("Me", syncStrategy);
|
|
512
|
-
}
|
|
513
|
-
static DOC_IDS = {
|
|
514
|
-
CONFIG: "CONFIG",
|
|
515
|
-
COURSE_REGISTRATIONS: "CourseRegistrations",
|
|
516
|
-
CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
|
|
517
|
-
};
|
|
518
|
-
// private email: string;
|
|
519
|
-
_username;
|
|
520
|
-
syncStrategy;
|
|
521
|
-
getUsername() {
|
|
522
|
-
return this._username;
|
|
523
|
-
}
|
|
524
|
-
isLoggedIn() {
|
|
525
|
-
return !this._username.startsWith(GuestUsername);
|
|
526
|
-
}
|
|
527
|
-
remoteDB;
|
|
528
|
-
remote() {
|
|
529
|
-
return this.remoteDB;
|
|
530
|
-
}
|
|
531
|
-
localDB;
|
|
532
|
-
updateQueue;
|
|
533
|
-
async createAccount(username, password) {
|
|
534
|
-
if (!this.syncStrategy.canCreateAccount()) {
|
|
535
|
-
throw new Error("Account creation not supported by current sync strategy");
|
|
536
|
-
}
|
|
537
|
-
if (!this._username.startsWith(GuestUsername)) {
|
|
538
|
-
throw new Error(
|
|
539
|
-
`Cannot create a new account while logged in:
|
|
540
|
-
Currently logged-in as ${this._username}.`
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
const result = await this.syncStrategy.createAccount(username, password);
|
|
544
|
-
if (result.status === Status.ok) {
|
|
545
|
-
log3(`Account created successfully, updating username to ${username}`);
|
|
546
|
-
this._username = username;
|
|
547
|
-
localStorage.removeItem("dbUUID");
|
|
548
|
-
await this.init();
|
|
549
|
-
}
|
|
550
|
-
return {
|
|
551
|
-
status: result.status,
|
|
552
|
-
error: result.error || ""
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
async login(username, password) {
|
|
556
|
-
if (!this.syncStrategy.canAuthenticate()) {
|
|
557
|
-
throw new Error("Authentication not supported by current sync strategy");
|
|
558
|
-
}
|
|
559
|
-
if (!this._username.startsWith(GuestUsername)) {
|
|
560
|
-
throw new Error(`Cannot change accounts while logged in.
|
|
561
|
-
Log out of account ${this.getUsername()} before logging in as ${username}.`);
|
|
562
|
-
}
|
|
563
|
-
const loginResult = await this.syncStrategy.authenticate(username, password);
|
|
564
|
-
if (loginResult.ok) {
|
|
565
|
-
log3(`Logged in as ${username}`);
|
|
566
|
-
this._username = username;
|
|
567
|
-
localStorage.removeItem("dbUUID");
|
|
568
|
-
await this.init();
|
|
569
|
-
}
|
|
570
|
-
return loginResult;
|
|
571
|
-
}
|
|
572
|
-
async resetUserData() {
|
|
573
|
-
if (this.syncStrategy.canAuthenticate()) {
|
|
574
|
-
return {
|
|
575
|
-
status: Status.error,
|
|
576
|
-
error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
try {
|
|
580
|
-
const localDB = getLocalUserDB(this._username);
|
|
581
|
-
const allDocs = await localDB.allDocs({ include_docs: false });
|
|
582
|
-
const docsToDelete = allDocs.rows.filter((row) => {
|
|
583
|
-
const id = row.id;
|
|
584
|
-
return id.startsWith(cardHistoryPrefix2) || // Card interaction history
|
|
585
|
-
id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
|
|
586
|
-
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
587
|
-
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
588
|
-
id === _BaseUser.DOC_IDS.CONFIG;
|
|
589
|
-
}).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
|
|
590
|
-
if (docsToDelete.length > 0) {
|
|
591
|
-
await localDB.bulkDocs(docsToDelete);
|
|
592
|
-
}
|
|
593
|
-
await this.init();
|
|
594
|
-
return { status: Status.ok };
|
|
595
|
-
} catch (error) {
|
|
596
|
-
logger.error("Failed to reset user data:", error);
|
|
597
|
-
return {
|
|
598
|
-
status: Status.error,
|
|
599
|
-
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
async logout() {
|
|
604
|
-
if (!this.syncStrategy.canAuthenticate()) {
|
|
605
|
-
this._username = await this.syncStrategy.getCurrentUsername();
|
|
606
|
-
await this.init();
|
|
607
|
-
return { ok: true };
|
|
608
|
-
}
|
|
609
|
-
const ret = await this.syncStrategy.logout();
|
|
610
|
-
this._username = await this.syncStrategy.getCurrentUsername();
|
|
611
|
-
await this.init();
|
|
612
|
-
return ret;
|
|
613
|
-
}
|
|
614
|
-
update(id, update) {
|
|
615
|
-
return this.updateQueue.update(id, update);
|
|
616
|
-
}
|
|
617
|
-
async getCourseRegistrationsDoc() {
|
|
618
|
-
logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
|
|
619
|
-
let ret;
|
|
620
|
-
try {
|
|
621
|
-
const regDoc = await this.localDB.get(
|
|
622
|
-
_BaseUser.DOC_IDS.COURSE_REGISTRATIONS
|
|
623
|
-
);
|
|
624
|
-
return regDoc;
|
|
625
|
-
} catch (e) {
|
|
626
|
-
const err = e;
|
|
627
|
-
if (err.status === 404) {
|
|
628
|
-
await this.localDB.put({
|
|
629
|
-
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
630
|
-
courses: [],
|
|
631
|
-
studyWeight: {}
|
|
632
|
-
});
|
|
633
|
-
ret = await this.getCourseRegistrationsDoc();
|
|
634
|
-
} else {
|
|
635
|
-
throw new Error(
|
|
636
|
-
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
return ret;
|
|
641
|
-
}
|
|
642
|
-
async getActiveCourses() {
|
|
643
|
-
const reg = await this.getCourseRegistrationsDoc();
|
|
644
|
-
return reg.courses.filter((c) => {
|
|
645
|
-
return c.status === void 0 || c.status === "active";
|
|
646
|
-
});
|
|
647
|
-
}
|
|
646
|
+
init_();
|
|
647
|
+
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
648
|
+
Navigators2["ELO"] = "elo";
|
|
649
|
+
return Navigators2;
|
|
650
|
+
})(Navigators || {});
|
|
651
|
+
ContentNavigator = class {
|
|
648
652
|
/**
|
|
649
|
-
* Returns a promise of the card IDs that the user has
|
|
650
|
-
* a scheduled review for.
|
|
651
653
|
*
|
|
654
|
+
* @param user
|
|
655
|
+
* @param strategyData
|
|
656
|
+
* @returns the runtime object used to steer a study session.
|
|
652
657
|
*/
|
|
653
|
-
async
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const hist = await this.getHistory();
|
|
665
|
-
const allRecords = [];
|
|
666
|
-
if (!Array.isArray(hist)) {
|
|
667
|
-
logger.error("getHistory did not return an array:", hist);
|
|
668
|
-
return allRecords;
|
|
669
|
-
}
|
|
670
|
-
let sampleCount = 0;
|
|
671
|
-
for (let i = 0; i < hist.length; i++) {
|
|
672
|
-
try {
|
|
673
|
-
if (hist[i] && Array.isArray(hist[i].records)) {
|
|
674
|
-
hist[i].records.forEach((record) => {
|
|
675
|
-
try {
|
|
676
|
-
if (!record.timeStamp) {
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
let timeStamp;
|
|
680
|
-
if (typeof record.timeStamp === "object") {
|
|
681
|
-
if (typeof record.timeStamp.toDate === "function") {
|
|
682
|
-
timeStamp = record.timeStamp.toISOString();
|
|
683
|
-
} else if (record.timeStamp instanceof Date) {
|
|
684
|
-
timeStamp = record.timeStamp.toISOString();
|
|
685
|
-
} else {
|
|
686
|
-
if (sampleCount < 3) {
|
|
687
|
-
logger.warn("Unknown timestamp object type:", record.timeStamp);
|
|
688
|
-
sampleCount++;
|
|
689
|
-
}
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
} else if (typeof record.timeStamp === "string") {
|
|
693
|
-
const date = new Date(record.timeStamp);
|
|
694
|
-
if (isNaN(date.getTime())) {
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
timeStamp = record.timeStamp;
|
|
698
|
-
} else if (typeof record.timeStamp === "number") {
|
|
699
|
-
timeStamp = new Date(record.timeStamp).toISOString();
|
|
700
|
-
} else {
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
allRecords.push({
|
|
704
|
-
timeStamp,
|
|
705
|
-
courseID: record.courseID || "unknown",
|
|
706
|
-
cardID: record.cardID || "unknown",
|
|
707
|
-
timeSpent: record.timeSpent || 0,
|
|
708
|
-
type: "card_view"
|
|
709
|
-
});
|
|
710
|
-
} catch (err) {
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
} catch (err) {
|
|
715
|
-
logger.error("Error processing history item:", err);
|
|
716
|
-
}
|
|
658
|
+
static async create(user, course, strategyData) {
|
|
659
|
+
const implementingClass = strategyData.implementingClass;
|
|
660
|
+
let NavigatorImpl;
|
|
661
|
+
const variations = ["", ".js", ".ts"];
|
|
662
|
+
for (const ext of variations) {
|
|
663
|
+
try {
|
|
664
|
+
const module = await globImport(`./${implementingClass}${ext}`);
|
|
665
|
+
NavigatorImpl = module.default;
|
|
666
|
+
break;
|
|
667
|
+
} catch (e) {
|
|
668
|
+
logger.debug(`Failed to load with extension ${ext}:`, e);
|
|
717
669
|
}
|
|
718
|
-
logger.debug(`Found ${allRecords.length} activity records`);
|
|
719
|
-
return allRecords;
|
|
720
|
-
} catch (err) {
|
|
721
|
-
logger.error("Error in getActivityRecords:", err);
|
|
722
|
-
return [];
|
|
723
670
|
}
|
|
671
|
+
if (!NavigatorImpl) {
|
|
672
|
+
throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
|
|
673
|
+
}
|
|
674
|
+
return new NavigatorImpl(user, course, strategyData);
|
|
724
675
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// src/impl/couch/courseDB.ts
|
|
681
|
+
import {
|
|
682
|
+
EloToNumber,
|
|
683
|
+
Status,
|
|
684
|
+
blankCourseElo as blankCourseElo2,
|
|
685
|
+
toCourseElo as toCourseElo2
|
|
686
|
+
} from "@vue-skuilder/common";
|
|
687
|
+
function randIntWeightedTowardZero(n) {
|
|
688
|
+
return Math.floor(Math.random() * Math.random() * Math.random() * n);
|
|
689
|
+
}
|
|
690
|
+
async function getCourseDataShapes(courseID) {
|
|
691
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
692
|
+
return cfg.dataShapes;
|
|
693
|
+
}
|
|
694
|
+
async function getCredentialledDataShapes(courseID) {
|
|
695
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
696
|
+
return cfg.dataShapes;
|
|
697
|
+
}
|
|
698
|
+
async function getCourseQuestionTypes(courseID) {
|
|
699
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
700
|
+
return cfg.questionTypes;
|
|
701
|
+
}
|
|
702
|
+
async function getCourseTagStubs(courseID) {
|
|
703
|
+
logger.debug(`Getting tag stubs for course: ${courseID}`);
|
|
704
|
+
const stubs = await filterAllDocsByPrefix(
|
|
705
|
+
getCourseDB2(courseID),
|
|
706
|
+
"TAG" /* TAG */.valueOf() + "-"
|
|
707
|
+
);
|
|
708
|
+
stubs.rows.forEach((row) => {
|
|
709
|
+
logger.debug(` Tag stub for doc: ${row.id}`);
|
|
710
|
+
});
|
|
711
|
+
return stubs;
|
|
712
|
+
}
|
|
713
|
+
async function deleteTag(courseID, tagName) {
|
|
714
|
+
tagName = getTagID(tagName);
|
|
715
|
+
const courseDB = getCourseDB2(courseID);
|
|
716
|
+
const doc = await courseDB.get("TAG" /* TAG */.valueOf() + "-" + tagName);
|
|
717
|
+
const resp = await courseDB.remove(doc);
|
|
718
|
+
return resp;
|
|
719
|
+
}
|
|
720
|
+
async function createTag(courseID, tagName, author) {
|
|
721
|
+
logger.debug(`Creating tag: ${tagName}...`);
|
|
722
|
+
const tagID = getTagID(tagName);
|
|
723
|
+
const courseDB = getCourseDB2(courseID);
|
|
724
|
+
const resp = await courseDB.put({
|
|
725
|
+
course: courseID,
|
|
726
|
+
docType: "TAG" /* TAG */,
|
|
727
|
+
name: tagName,
|
|
728
|
+
snippet: "",
|
|
729
|
+
taggedCards: [],
|
|
730
|
+
wiki: "",
|
|
731
|
+
author,
|
|
732
|
+
_id: tagID
|
|
733
|
+
});
|
|
734
|
+
return resp;
|
|
735
|
+
}
|
|
736
|
+
async function updateTag(tag) {
|
|
737
|
+
const prior = await getTag(tag.course, tag.name);
|
|
738
|
+
return await getCourseDB2(tag.course).put({
|
|
739
|
+
...tag,
|
|
740
|
+
_rev: prior._rev
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
async function getTag(courseID, tagName) {
|
|
744
|
+
const tagID = getTagID(tagName);
|
|
745
|
+
const courseDB = getCourseDB2(courseID);
|
|
746
|
+
return courseDB.get(tagID);
|
|
747
|
+
}
|
|
748
|
+
async function removeTagFromCard(courseID, cardID, tagID) {
|
|
749
|
+
tagID = getTagID(tagID);
|
|
750
|
+
const courseDB = getCourseDB2(courseID);
|
|
751
|
+
const tag = await courseDB.get(tagID);
|
|
752
|
+
tag.taggedCards = tag.taggedCards.filter((taggedID) => {
|
|
753
|
+
return cardID !== taggedID;
|
|
754
|
+
});
|
|
755
|
+
return courseDB.put(tag);
|
|
756
|
+
}
|
|
757
|
+
function getAncestorTagIDs(courseID, tagID) {
|
|
758
|
+
tagID = getTagID(tagID);
|
|
759
|
+
const split = tagID.split(">");
|
|
760
|
+
if (split.length === 1) {
|
|
761
|
+
return [];
|
|
762
|
+
} else {
|
|
763
|
+
split.pop();
|
|
764
|
+
const parent = split.join(">");
|
|
765
|
+
return [parent].concat(getAncestorTagIDs(courseID, parent));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async function getChildTagStubs(courseID, tagID) {
|
|
769
|
+
return await filterAllDocsByPrefix(getCourseDB2(courseID), tagID + ">");
|
|
770
|
+
}
|
|
771
|
+
async function getAppliedTags(id_course, id_card) {
|
|
772
|
+
const db = getCourseDB2(id_course);
|
|
773
|
+
const result = await db.query("getTags", {
|
|
774
|
+
startkey: id_card,
|
|
775
|
+
endkey: id_card
|
|
776
|
+
// include_docs: true
|
|
777
|
+
});
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
async function updateCardElo2(courseID, cardID, elo) {
|
|
781
|
+
if (elo) {
|
|
782
|
+
const cDB = getCourseDB2(courseID);
|
|
783
|
+
const card = await cDB.get(cardID);
|
|
784
|
+
logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
|
|
785
|
+
card.elo = elo;
|
|
786
|
+
return cDB.put(card);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async function updateCredentialledCourseConfig(courseID, config) {
|
|
790
|
+
logger.debug(`Updating course config:
|
|
791
|
+
|
|
792
|
+
${JSON.stringify(config)}
|
|
793
|
+
`);
|
|
794
|
+
const db = getCourseDB2(courseID);
|
|
795
|
+
const old = await getCredentialledCourseConfig(courseID);
|
|
796
|
+
return await db.put({
|
|
797
|
+
...config,
|
|
798
|
+
_rev: old._rev
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
function isSuccessRow(row) {
|
|
802
|
+
return "doc" in row && row.doc !== null && row.doc !== void 0;
|
|
803
|
+
}
|
|
804
|
+
var CoursesDB, CourseDB;
|
|
805
|
+
var init_courseDB = __esm({
|
|
806
|
+
"src/impl/couch/courseDB.ts"() {
|
|
807
|
+
"use strict";
|
|
808
|
+
init_couch();
|
|
809
|
+
init_updateQueue();
|
|
810
|
+
init_types_legacy();
|
|
811
|
+
init_logger();
|
|
812
|
+
init_clientCache();
|
|
813
|
+
init_courseAPI();
|
|
814
|
+
init_courseLookupDB();
|
|
815
|
+
init_navigators();
|
|
816
|
+
CoursesDB = class {
|
|
817
|
+
_courseIDs;
|
|
818
|
+
constructor(courseIDs) {
|
|
819
|
+
if (courseIDs && courseIDs.length > 0) {
|
|
820
|
+
this._courseIDs = courseIDs;
|
|
768
821
|
} else {
|
|
769
|
-
|
|
822
|
+
this._courseIDs = void 0;
|
|
770
823
|
}
|
|
771
824
|
}
|
|
772
|
-
async
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
user: true,
|
|
780
|
-
admin: false,
|
|
781
|
-
moderator: false,
|
|
782
|
-
elo: {
|
|
783
|
-
global: {
|
|
784
|
-
score: 1e3,
|
|
785
|
-
count: 0
|
|
786
|
-
},
|
|
787
|
-
tags: {},
|
|
788
|
-
misc: {}
|
|
789
|
-
}
|
|
790
|
-
};
|
|
791
|
-
if (doc.courses.filter((course) => {
|
|
792
|
-
return course.courseID === regItem.courseID;
|
|
793
|
-
}).length === 0) {
|
|
794
|
-
log3(`It's a new course registration!`);
|
|
795
|
-
doc.courses.push(regItem);
|
|
796
|
-
doc.studyWeight[course_id] = 1;
|
|
797
|
-
} else {
|
|
798
|
-
doc.courses.forEach((c) => {
|
|
799
|
-
log3(`Found the previously registered course!`);
|
|
800
|
-
if (c.courseID === course_id) {
|
|
801
|
-
c.status = status;
|
|
802
|
-
}
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
return this.localDB.put(doc);
|
|
806
|
-
}).catch((e) => {
|
|
807
|
-
log3(`Registration failed because of: ${JSON.stringify(e)}`);
|
|
808
|
-
throw e;
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
async dropCourse(course_id, dropStatus = "dropped") {
|
|
812
|
-
return this.getCourseRegistrationsDoc().then((doc) => {
|
|
813
|
-
let index = -1;
|
|
814
|
-
for (let i = 0; i < doc.courses.length; i++) {
|
|
815
|
-
if (doc.courses[i].courseID === course_id) {
|
|
816
|
-
index = i;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
if (index !== -1) {
|
|
820
|
-
delete doc.studyWeight[course_id];
|
|
821
|
-
doc.courses[index].status = dropStatus;
|
|
822
|
-
} else {
|
|
823
|
-
throw new Error(
|
|
824
|
-
`User ${this.getUsername()} is not currently registered for course ${course_id}`
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
return this.localDB.put(doc);
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
async getCourseInterface(courseId) {
|
|
831
|
-
return new UsrCrsData(this, courseId);
|
|
832
|
-
}
|
|
833
|
-
async getUserEditableCourses() {
|
|
834
|
-
let courseIDs = [];
|
|
835
|
-
const registeredCourses = await this.getCourseRegistrationsDoc();
|
|
836
|
-
courseIDs = courseIDs.concat(
|
|
837
|
-
registeredCourses.courses.map((course) => {
|
|
838
|
-
return course.courseID;
|
|
839
|
-
})
|
|
840
|
-
);
|
|
825
|
+
async getCourseList() {
|
|
826
|
+
let crsList = await CourseLookup.allCourses();
|
|
827
|
+
logger.debug(`AllCourses: ${crsList.map((c) => c.name + ", " + c._id + "\n ")}`);
|
|
828
|
+
if (this._courseIDs) {
|
|
829
|
+
crsList = crsList.filter((c) => this._courseIDs.includes(c._id));
|
|
830
|
+
}
|
|
831
|
+
logger.debug(`AllCourses.filtered: ${crsList.map((c) => c.name + ", " + c._id + "\n ")}`);
|
|
841
832
|
const cfgs = await Promise.all(
|
|
842
|
-
|
|
843
|
-
|
|
833
|
+
crsList.map(async (c) => {
|
|
834
|
+
try {
|
|
835
|
+
const cfg = await getCredentialledCourseConfig(c._id);
|
|
836
|
+
logger.debug(`Found cfg: ${JSON.stringify(cfg)}`);
|
|
837
|
+
return cfg;
|
|
838
|
+
} catch (e) {
|
|
839
|
+
logger.warn(`Error fetching cfg for course ${c.name}, ${c._id}: ${e}`);
|
|
840
|
+
return void 0;
|
|
841
|
+
}
|
|
844
842
|
})
|
|
845
843
|
);
|
|
846
|
-
return cfgs;
|
|
844
|
+
return cfgs.filter((c) => !!c);
|
|
847
845
|
}
|
|
848
|
-
async
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
darkMode: false,
|
|
852
|
-
likesConfetti: false
|
|
853
|
-
};
|
|
854
|
-
try {
|
|
855
|
-
const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
|
|
856
|
-
logger.debug("Raw config from DB:", cfg);
|
|
857
|
-
return cfg;
|
|
858
|
-
} catch (e) {
|
|
859
|
-
const err = e;
|
|
860
|
-
if (err.name && err.name === "not_found") {
|
|
861
|
-
await this.localDB.put(defaultConfig);
|
|
862
|
-
return this.getConfig();
|
|
863
|
-
} else {
|
|
864
|
-
logger.error(`Error setting user default config:`, e);
|
|
865
|
-
throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
|
|
866
|
-
}
|
|
846
|
+
async getCourseConfig(courseId) {
|
|
847
|
+
if (this._courseIDs && this._courseIDs.length && !this._courseIDs.includes(courseId)) {
|
|
848
|
+
throw new Error(`Course ${courseId} not in course list`);
|
|
867
849
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const c = await this.getConfig();
|
|
872
|
-
const put = await this.localDB.put({
|
|
873
|
-
...c,
|
|
874
|
-
...items
|
|
875
|
-
});
|
|
876
|
-
if (put.ok) {
|
|
877
|
-
logger.debug(`Config items set: ${JSON.stringify(items)}`);
|
|
850
|
+
const cfg = await getCredentialledCourseConfig(courseId);
|
|
851
|
+
if (cfg === void 0) {
|
|
852
|
+
throw new Error(`Error fetching cfg for course ${courseId}`);
|
|
878
853
|
} else {
|
|
879
|
-
|
|
854
|
+
return cfg;
|
|
880
855
|
}
|
|
881
856
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
* This function should be called *only* by the pouchdb datalayer provider
|
|
885
|
-
* auth store.
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
* Anyone else seeking the current user should use the auth store's
|
|
889
|
-
* exported `getCurrentUser` method.
|
|
890
|
-
*
|
|
891
|
-
*/
|
|
892
|
-
static async instance(syncStrategy, username) {
|
|
893
|
-
if (username) {
|
|
894
|
-
_BaseUser._instance = new _BaseUser(username, syncStrategy);
|
|
895
|
-
await _BaseUser._instance.init();
|
|
896
|
-
return _BaseUser._instance;
|
|
897
|
-
} else if (_BaseUser._instance && _BaseUser._initialized) {
|
|
898
|
-
return _BaseUser._instance;
|
|
899
|
-
} else if (_BaseUser._instance) {
|
|
900
|
-
return new Promise((resolve) => {
|
|
901
|
-
(function waitForUser() {
|
|
902
|
-
if (_BaseUser._initialized) {
|
|
903
|
-
return resolve(_BaseUser._instance);
|
|
904
|
-
} else {
|
|
905
|
-
setTimeout(waitForUser, 50);
|
|
906
|
-
}
|
|
907
|
-
})();
|
|
908
|
-
});
|
|
909
|
-
} else {
|
|
910
|
-
const guestUsername = await syncStrategy.getCurrentUsername();
|
|
911
|
-
_BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
|
|
912
|
-
await _BaseUser._instance.init();
|
|
913
|
-
return _BaseUser._instance;
|
|
914
|
-
}
|
|
857
|
+
async disambiguateCourse(courseId, disambiguator) {
|
|
858
|
+
await CourseLookup.updateDisambiguator(courseId, disambiguator);
|
|
915
859
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
860
|
+
};
|
|
861
|
+
CourseDB = class {
|
|
862
|
+
// private log(msg: string): void {
|
|
863
|
+
// log(`CourseLog: ${this.id}\n ${msg}`);
|
|
864
|
+
// }
|
|
865
|
+
db;
|
|
866
|
+
id;
|
|
867
|
+
_getCurrentUser;
|
|
868
|
+
updateQueue;
|
|
869
|
+
constructor(id, userLookup) {
|
|
870
|
+
this.id = id;
|
|
871
|
+
this.db = getCourseDB2(this.id);
|
|
872
|
+
this._getCurrentUser = userLookup;
|
|
873
|
+
this.updateQueue = new UpdateQueue(this.db);
|
|
921
874
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
|
|
925
|
-
this.updateQueue = new UpdateQueue(this.localDB);
|
|
875
|
+
getCourseID() {
|
|
876
|
+
return this.id;
|
|
926
877
|
}
|
|
927
|
-
async
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
878
|
+
async getCourseInfo() {
|
|
879
|
+
const cardCount = (await this.db.find({
|
|
880
|
+
selector: {
|
|
881
|
+
docType: "CARD" /* CARD */
|
|
882
|
+
},
|
|
883
|
+
limit: 1e3
|
|
884
|
+
})).docs.length;
|
|
885
|
+
return {
|
|
886
|
+
cardCount,
|
|
887
|
+
registeredUsers: 0
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
async getInexperiencedCards(limit = 2) {
|
|
891
|
+
return (await this.db.query("cardsByInexperience", {
|
|
892
|
+
limit
|
|
893
|
+
})).rows.map((r) => {
|
|
894
|
+
const ret = {
|
|
895
|
+
courseId: this.id,
|
|
896
|
+
cardId: r.id,
|
|
897
|
+
count: r.key,
|
|
898
|
+
elo: r.value
|
|
899
|
+
};
|
|
900
|
+
return ret;
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
async getCardsByEloLimits(options = {
|
|
904
|
+
low: 0,
|
|
905
|
+
high: Number.MIN_SAFE_INTEGER,
|
|
906
|
+
limit: 25,
|
|
907
|
+
page: 0
|
|
908
|
+
}) {
|
|
909
|
+
return (await this.db.query("elo", {
|
|
910
|
+
startkey: options.low,
|
|
911
|
+
endkey: options.high,
|
|
912
|
+
limit: options.limit,
|
|
913
|
+
skip: options.limit * options.page
|
|
914
|
+
})).rows.map((r) => {
|
|
915
|
+
return `${this.id}-${r.id}-${r.key}`;
|
|
916
|
+
});
|
|
934
917
|
}
|
|
935
|
-
|
|
936
|
-
{
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
];
|
|
949
|
-
async applyDesignDocs() {
|
|
950
|
-
for (const doc of _BaseUser.designDocs) {
|
|
951
|
-
try {
|
|
952
|
-
try {
|
|
953
|
-
const existingDoc = await this.remoteDB.get(doc._id);
|
|
954
|
-
await this.remoteDB.put({
|
|
955
|
-
...doc,
|
|
956
|
-
_rev: existingDoc._rev
|
|
957
|
-
});
|
|
958
|
-
} catch (e) {
|
|
959
|
-
if (e instanceof Error && e.name === "not_found") {
|
|
960
|
-
await this.remoteDB.put(doc);
|
|
961
|
-
} else {
|
|
962
|
-
throw e;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
} catch (error) {
|
|
966
|
-
if (error instanceof Error && error.name === "conflict") {
|
|
967
|
-
logger.warn(`Design doc ${doc._id} update conflict - will retry`);
|
|
968
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
969
|
-
await this.applyDesignDoc(doc);
|
|
918
|
+
async getCardEloData(id) {
|
|
919
|
+
const docs = await this.db.allDocs({
|
|
920
|
+
keys: id,
|
|
921
|
+
include_docs: true
|
|
922
|
+
});
|
|
923
|
+
const ret = [];
|
|
924
|
+
docs.rows.forEach((r) => {
|
|
925
|
+
if (isSuccessRow(r)) {
|
|
926
|
+
if (r.doc && r.doc.elo) {
|
|
927
|
+
ret.push(toCourseElo2(r.doc.elo));
|
|
970
928
|
} else {
|
|
971
|
-
logger.
|
|
972
|
-
|
|
929
|
+
logger.warn("no elo data for card: " + r.id);
|
|
930
|
+
ret.push(blankCourseElo2());
|
|
973
931
|
}
|
|
932
|
+
} else {
|
|
933
|
+
logger.warn("no elo data for card: " + JSON.stringify(r));
|
|
934
|
+
ret.push(blankCourseElo2());
|
|
974
935
|
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// Helper method for single doc update with retry
|
|
978
|
-
async applyDesignDoc(doc, retries = 3) {
|
|
979
|
-
try {
|
|
980
|
-
const existingDoc = await this.remoteDB.get(doc._id);
|
|
981
|
-
await this.remoteDB.put({
|
|
982
|
-
...doc,
|
|
983
|
-
_rev: existingDoc._rev
|
|
984
|
-
});
|
|
985
|
-
} catch (e) {
|
|
986
|
-
if (e instanceof Error && e.name === "conflict" && retries > 0) {
|
|
987
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
988
|
-
return this.applyDesignDoc(doc, retries - 1);
|
|
989
|
-
}
|
|
990
|
-
throw e;
|
|
991
|
-
}
|
|
936
|
+
});
|
|
937
|
+
return ret;
|
|
992
938
|
}
|
|
993
939
|
/**
|
|
994
|
-
*
|
|
995
|
-
* up-to-date history
|
|
996
|
-
*
|
|
997
|
-
* // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
|
|
998
|
-
*
|
|
999
|
-
* @param record the recent recorded interaction between user and card
|
|
1000
|
-
* @returns The updated state of the card's CardHistory data
|
|
940
|
+
* Returns the lowest and highest `global` ELO ratings in the course
|
|
1001
941
|
*/
|
|
1002
|
-
async
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
942
|
+
async getELOBounds() {
|
|
943
|
+
const [low, high] = await Promise.all([
|
|
944
|
+
(await this.db.query("elo", {
|
|
945
|
+
startkey: 0,
|
|
946
|
+
limit: 1,
|
|
947
|
+
include_docs: false
|
|
948
|
+
})).rows[0].key,
|
|
949
|
+
(await this.db.query("elo", {
|
|
950
|
+
limit: 1,
|
|
951
|
+
descending: true,
|
|
952
|
+
startkey: 1e5
|
|
953
|
+
})).rows[0].key
|
|
954
|
+
]);
|
|
955
|
+
return {
|
|
956
|
+
low,
|
|
957
|
+
high
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
async removeCard(id) {
|
|
961
|
+
const doc = await this.db.get(id);
|
|
962
|
+
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
963
|
+
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
964
|
+
}
|
|
965
|
+
return this.db.remove(doc);
|
|
966
|
+
}
|
|
967
|
+
async getCardDisplayableDataIDs(id) {
|
|
968
|
+
logger.debug(id.join(", "));
|
|
969
|
+
const cards = await this.db.allDocs({
|
|
970
|
+
keys: id,
|
|
971
|
+
include_docs: true
|
|
972
|
+
});
|
|
973
|
+
const ret = {};
|
|
974
|
+
cards.rows.forEach((r) => {
|
|
975
|
+
if (isSuccessRow(r)) {
|
|
976
|
+
ret[r.id] = r.doc.id_displayable_data;
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
await Promise.all(
|
|
980
|
+
cards.rows.map((r) => {
|
|
981
|
+
return async () => {
|
|
982
|
+
if (isSuccessRow(r)) {
|
|
983
|
+
ret[r.id] = r.doc.id_displayable_data;
|
|
984
|
+
}
|
|
1035
985
|
};
|
|
1036
|
-
|
|
1037
|
-
|
|
986
|
+
})
|
|
987
|
+
);
|
|
988
|
+
return ret;
|
|
989
|
+
}
|
|
990
|
+
async getCardsByELO(elo, cardLimit) {
|
|
991
|
+
elo = parseInt(elo);
|
|
992
|
+
const limit = cardLimit ? cardLimit : 25;
|
|
993
|
+
const below = await this.db.query("elo", {
|
|
994
|
+
limit: Math.ceil(limit / 2),
|
|
995
|
+
startkey: elo,
|
|
996
|
+
descending: true
|
|
997
|
+
});
|
|
998
|
+
const aboveLimit = limit - below.rows.length;
|
|
999
|
+
const above = await this.db.query("elo", {
|
|
1000
|
+
limit: aboveLimit,
|
|
1001
|
+
startkey: elo + 1
|
|
1002
|
+
});
|
|
1003
|
+
let cards = below.rows;
|
|
1004
|
+
cards = cards.concat(above.rows);
|
|
1005
|
+
const ret = cards.sort((a, b) => {
|
|
1006
|
+
const s = Math.abs(a.key - elo) - Math.abs(b.key - elo);
|
|
1007
|
+
if (s === 0) {
|
|
1008
|
+
return Math.random() - 0.5;
|
|
1038
1009
|
} else {
|
|
1039
|
-
|
|
1040
|
-
name:${reason.name}
|
|
1041
|
-
error: ${reason.error}
|
|
1042
|
-
message: ${reason.message}`);
|
|
1010
|
+
return s;
|
|
1043
1011
|
}
|
|
1012
|
+
}).map((c) => `${this.id}-${c.id}-${c.key}`);
|
|
1013
|
+
const str = `below:
|
|
1014
|
+
${below.rows.map((r) => ` ${r.id}-${r.key}
|
|
1015
|
+
`)}
|
|
1016
|
+
|
|
1017
|
+
above:
|
|
1018
|
+
${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
1019
|
+
`)}`;
|
|
1020
|
+
logger.debug(`Getting ${limit} cards centered around elo: ${elo}:
|
|
1021
|
+
|
|
1022
|
+
` + str);
|
|
1023
|
+
return ret;
|
|
1024
|
+
}
|
|
1025
|
+
async getCourseConfig() {
|
|
1026
|
+
const ret = await getCredentialledCourseConfig(this.id);
|
|
1027
|
+
if (ret) {
|
|
1028
|
+
return ret;
|
|
1029
|
+
} else {
|
|
1030
|
+
throw new Error(`Course config not found for course ID: ${this.id}`);
|
|
1044
1031
|
}
|
|
1045
1032
|
}
|
|
1046
|
-
async
|
|
1033
|
+
async updateCourseConfig(cfg) {
|
|
1034
|
+
logger.debug(`Updating: ${JSON.stringify(cfg)}`);
|
|
1047
1035
|
try {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
if (duplicateDocIds.length > 0) {
|
|
1068
|
-
log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
|
|
1069
|
-
const deletePromises = duplicateDocIds.map(async (docId) => {
|
|
1070
|
-
try {
|
|
1071
|
-
const doc = await this.remoteDB.get(docId);
|
|
1072
|
-
await this.remoteDB.remove(doc);
|
|
1073
|
-
log3(`Successfully removed duplicate review: ${docId}`);
|
|
1074
|
-
} catch (error) {
|
|
1075
|
-
log3(`Failed to remove duplicate review ${docId}: ${error}`);
|
|
1076
|
-
}
|
|
1077
|
-
});
|
|
1078
|
-
await Promise.all(deletePromises);
|
|
1079
|
-
log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
|
|
1080
|
-
} else {
|
|
1081
|
-
log3("No duplicate reviews found");
|
|
1082
|
-
}
|
|
1036
|
+
return await updateCredentialledCourseConfig(this.id, cfg);
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
logger.error(`Error updating course config in course DB: ${error}`);
|
|
1039
|
+
throw error;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
async updateCardElo(cardId, elo) {
|
|
1043
|
+
if (!elo) {
|
|
1044
|
+
throw new Error(`Cannot update card elo with null or undefined value for card ID: ${cardId}`);
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
const result = await this.updateQueue.update(cardId, (card) => {
|
|
1048
|
+
logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
|
|
1049
|
+
card.elo = elo;
|
|
1050
|
+
return card;
|
|
1051
|
+
});
|
|
1052
|
+
return { ok: true, id: cardId, rev: result._rev };
|
|
1083
1053
|
} catch (error) {
|
|
1084
|
-
|
|
1054
|
+
logger.error(`Failed to update card elo for card ID: ${cardId}`, error);
|
|
1055
|
+
throw new Error(`Failed to update card elo for card ID: ${cardId}`);
|
|
1085
1056
|
}
|
|
1086
1057
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
async getSeenCards(course_id) {
|
|
1094
|
-
let prefix = cardHistoryPrefix2;
|
|
1095
|
-
if (course_id) {
|
|
1096
|
-
prefix += course_id;
|
|
1058
|
+
async getAppliedTags(cardId) {
|
|
1059
|
+
const ret = await getAppliedTags(this.id, cardId);
|
|
1060
|
+
if (ret) {
|
|
1061
|
+
return ret;
|
|
1062
|
+
} else {
|
|
1063
|
+
throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
|
|
1097
1064
|
}
|
|
1098
|
-
const docs = await filterAllDocsByPrefix(this.localDB, prefix, {
|
|
1099
|
-
include_docs: false
|
|
1100
|
-
});
|
|
1101
|
-
const ret = [];
|
|
1102
|
-
docs.rows.forEach((row) => {
|
|
1103
|
-
if (row.id.startsWith(cardHistoryPrefix2)) {
|
|
1104
|
-
ret.push(row.id.substr(cardHistoryPrefix2.length));
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
return ret;
|
|
1108
1065
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
* @returns A promise of the cards that the user has seen in the past.
|
|
1112
|
-
*/
|
|
1113
|
-
async getHistory() {
|
|
1114
|
-
const cards = await filterAllDocsByPrefix(
|
|
1115
|
-
this.remoteDB,
|
|
1116
|
-
cardHistoryPrefix2,
|
|
1117
|
-
{
|
|
1118
|
-
include_docs: true,
|
|
1119
|
-
attachments: false
|
|
1120
|
-
}
|
|
1121
|
-
);
|
|
1122
|
-
return cards.rows.map((r) => r.doc);
|
|
1066
|
+
async addTagToCard(cardId, tagId, updateELO) {
|
|
1067
|
+
return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
|
|
1123
1068
|
}
|
|
1124
|
-
async
|
|
1125
|
-
|
|
1126
|
-
const crs = doc.courses.find((c) => c.courseID === course_id);
|
|
1127
|
-
if (crs) {
|
|
1128
|
-
if (crs.settings === null || crs.settings === void 0) {
|
|
1129
|
-
crs.settings = {};
|
|
1130
|
-
}
|
|
1131
|
-
settings.forEach((setting) => {
|
|
1132
|
-
crs.settings[setting.key] = setting.value;
|
|
1133
|
-
});
|
|
1134
|
-
}
|
|
1135
|
-
return this.localDB.put(doc);
|
|
1136
|
-
});
|
|
1069
|
+
async removeTagFromCard(cardId, tagId) {
|
|
1070
|
+
return await removeTagFromCard(this.id, cardId, tagId);
|
|
1137
1071
|
}
|
|
1138
|
-
async
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1072
|
+
async createTag(name, author) {
|
|
1073
|
+
return await createTag(this.id, name, author);
|
|
1074
|
+
}
|
|
1075
|
+
async getTag(tagId) {
|
|
1076
|
+
return await getTag(this.id, tagId);
|
|
1077
|
+
}
|
|
1078
|
+
async updateTag(tag) {
|
|
1079
|
+
if (tag.course !== this.id) {
|
|
1080
|
+
throw new Error(`Tag ${JSON.stringify(tag)} does not belong to course ${this.id}`);
|
|
1146
1081
|
}
|
|
1082
|
+
return await updateTag(tag);
|
|
1147
1083
|
}
|
|
1148
|
-
async
|
|
1149
|
-
|
|
1084
|
+
async getCourseTagStubs() {
|
|
1085
|
+
return getCourseTagStubs(this.id);
|
|
1086
|
+
}
|
|
1087
|
+
async addNote(codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo2()) {
|
|
1150
1088
|
try {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1089
|
+
const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
|
|
1090
|
+
if (resp.ok) {
|
|
1091
|
+
if (resp.cardCreationFailed) {
|
|
1092
|
+
logger.warn(
|
|
1093
|
+
`[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
|
|
1094
|
+
);
|
|
1095
|
+
return {
|
|
1096
|
+
status: Status.error,
|
|
1097
|
+
message: `Note was added but no cards were created: ${resp.cardCreationError}`,
|
|
1098
|
+
id: resp.id
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
status: Status.ok,
|
|
1103
|
+
message: "",
|
|
1104
|
+
id: resp.id
|
|
1105
|
+
};
|
|
1162
1106
|
} else {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
message: err.message,
|
|
1167
|
-
reason: err.reason,
|
|
1168
|
-
error: err.error
|
|
1107
|
+
return {
|
|
1108
|
+
status: Status.error,
|
|
1109
|
+
message: "Unexpected error adding note"
|
|
1169
1110
|
};
|
|
1170
|
-
logger.error(
|
|
1171
|
-
"Database error in getOrCreateClassroomRegistrationsDoc (private method):",
|
|
1172
|
-
errorDetails
|
|
1173
|
-
);
|
|
1174
|
-
throw new Error(
|
|
1175
|
-
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
1176
|
-
);
|
|
1177
1111
|
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
*
|
|
1185
|
-
* @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
|
|
1186
|
-
* registration document is unavailable due to database errors
|
|
1187
|
-
*
|
|
1188
|
-
* @description This method gracefully handles database connectivity issues by returning
|
|
1189
|
-
* an empty array when the classroom registrations document cannot be accessed.
|
|
1190
|
-
* This ensures that users can still access other application features even
|
|
1191
|
-
* when classroom functionality is temporarily unavailable.
|
|
1192
|
-
*/
|
|
1193
|
-
async getActiveClasses() {
|
|
1194
|
-
try {
|
|
1195
|
-
return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
|
|
1196
|
-
} catch (error) {
|
|
1197
|
-
logger.warn(
|
|
1198
|
-
"Failed to load classroom registrations, continuing without classroom data:",
|
|
1199
|
-
error
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
const err = e;
|
|
1114
|
+
logger.error(
|
|
1115
|
+
`[addNote] error ${err.name}
|
|
1116
|
+
reason: ${err.reason}
|
|
1117
|
+
message: ${err.message}`
|
|
1200
1118
|
);
|
|
1201
|
-
return
|
|
1119
|
+
return {
|
|
1120
|
+
status: Status.error,
|
|
1121
|
+
message: `Error adding note to course. ${e.reason || err.message}`
|
|
1122
|
+
};
|
|
1202
1123
|
}
|
|
1203
1124
|
}
|
|
1204
|
-
async
|
|
1205
|
-
return
|
|
1125
|
+
async getCourseDoc(id, options) {
|
|
1126
|
+
return await getCourseDoc(this.id, id, options);
|
|
1206
1127
|
}
|
|
1207
|
-
async
|
|
1208
|
-
return
|
|
1128
|
+
async getCourseDocs(ids, options = {}) {
|
|
1129
|
+
return await getCourseDocs(this.id, ids, options);
|
|
1209
1130
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1131
|
+
////////////////////////////////////
|
|
1132
|
+
// NavigationStrategyManager implementation
|
|
1133
|
+
////////////////////////////////////
|
|
1134
|
+
getNavigationStrategy(id) {
|
|
1135
|
+
logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
|
|
1136
|
+
const strategy = {
|
|
1137
|
+
id: "ELO",
|
|
1138
|
+
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1139
|
+
name: "ELO",
|
|
1140
|
+
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
1141
|
+
implementingClass: "elo" /* ELO */,
|
|
1142
|
+
course: this.id,
|
|
1143
|
+
serializedData: ""
|
|
1144
|
+
// serde is a noop for ELO navigator.
|
|
1145
|
+
};
|
|
1146
|
+
return Promise.resolve(strategy);
|
|
1212
1147
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1148
|
+
getAllNavigationStrategies() {
|
|
1149
|
+
logger.debug("[courseDB] Returning hard-coded navigation strategies");
|
|
1150
|
+
const strategies = [
|
|
1151
|
+
{
|
|
1152
|
+
id: "ELO",
|
|
1153
|
+
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1154
|
+
name: "ELO",
|
|
1155
|
+
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
1156
|
+
implementingClass: "elo" /* ELO */,
|
|
1157
|
+
course: this.id,
|
|
1158
|
+
serializedData: ""
|
|
1159
|
+
// serde is a noop for ELO navigator.
|
|
1160
|
+
}
|
|
1161
|
+
];
|
|
1162
|
+
return Promise.resolve(strategies);
|
|
1215
1163
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1164
|
+
addNavigationStrategy(data) {
|
|
1165
|
+
logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
|
|
1166
|
+
logger.debug(JSON.stringify(data));
|
|
1167
|
+
return Promise.resolve();
|
|
1218
1168
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1169
|
+
updateNavigationStrategy(id, data) {
|
|
1170
|
+
logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
|
|
1171
|
+
logger.debug(JSON.stringify(data));
|
|
1172
|
+
return Promise.resolve();
|
|
1221
1173
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
// src/impl/couch/courseAPI.ts
|
|
1239
|
-
import { NameSpacer } from "@vue-skuilder/common";
|
|
1240
|
-
import { blankCourseElo, toCourseElo } from "@vue-skuilder/common";
|
|
1241
|
-
import { prepareNote55 } from "@vue-skuilder/common";
|
|
1242
|
-
async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo()) {
|
|
1243
|
-
const db = getCourseDB(courseID);
|
|
1244
|
-
const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads);
|
|
1245
|
-
const result = await db.post(payload);
|
|
1246
|
-
const dataShapeId = NameSpacer.getDataShapeString({
|
|
1247
|
-
course: codeCourse,
|
|
1248
|
-
dataShape: shape.name
|
|
1249
|
-
});
|
|
1250
|
-
if (result.ok) {
|
|
1251
|
-
try {
|
|
1252
|
-
await createCards(courseID, dataShapeId, result.id, tags, elo, author);
|
|
1253
|
-
} catch (error) {
|
|
1254
|
-
let errorMessage = "Unknown error";
|
|
1255
|
-
if (error instanceof Error) {
|
|
1256
|
-
errorMessage = error.message;
|
|
1257
|
-
} else if (error && typeof error === "object" && "reason" in error) {
|
|
1258
|
-
errorMessage = error.reason;
|
|
1259
|
-
} else if (error && typeof error === "object" && "message" in error) {
|
|
1260
|
-
errorMessage = error.message;
|
|
1261
|
-
} else {
|
|
1262
|
-
errorMessage = String(error);
|
|
1174
|
+
async surfaceNavigationStrategy() {
|
|
1175
|
+
logger.warn(`Returning hard-coded default ELO navigator`);
|
|
1176
|
+
const ret = {
|
|
1177
|
+
id: "ELO",
|
|
1178
|
+
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
1179
|
+
name: "ELO",
|
|
1180
|
+
description: "ELO-based navigation strategy",
|
|
1181
|
+
implementingClass: "elo" /* ELO */,
|
|
1182
|
+
course: this.id,
|
|
1183
|
+
serializedData: ""
|
|
1184
|
+
// serde is a noop for ELO navigator.
|
|
1185
|
+
};
|
|
1186
|
+
return Promise.resolve(ret);
|
|
1263
1187
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
if (questionViewTypes.length === 0) {
|
|
1283
|
-
const errorMsg = `No questionViewTypes found for datashapeID: ${datashapeID} in course config. Cards cannot be created.`;
|
|
1284
|
-
logger.error(errorMsg);
|
|
1285
|
-
throw new Error(errorMsg);
|
|
1286
|
-
}
|
|
1287
|
-
for (const questionView of questionViewTypes) {
|
|
1288
|
-
await createCard(questionView, courseID, dsDescriptor, noteID, tags, elo, author);
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags, elo = blankCourseElo(), author) {
|
|
1292
|
-
const qDescriptor = NameSpacer.getQuestionDescriptor(questionViewName);
|
|
1293
|
-
const cfg = await getCredentialledCourseConfig(courseID);
|
|
1294
|
-
for (const rQ of cfg.questionTypes) {
|
|
1295
|
-
if (rQ.name === questionViewName) {
|
|
1296
|
-
for (const view of rQ.viewList) {
|
|
1297
|
-
await addCard(
|
|
1298
|
-
courseID,
|
|
1299
|
-
dsDescriptor.course,
|
|
1300
|
-
[noteID],
|
|
1301
|
-
NameSpacer.getViewString({
|
|
1302
|
-
course: qDescriptor.course,
|
|
1303
|
-
questionType: qDescriptor.questionType,
|
|
1304
|
-
view
|
|
1305
|
-
}),
|
|
1306
|
-
elo,
|
|
1307
|
-
tags,
|
|
1308
|
-
author
|
|
1309
|
-
);
|
|
1188
|
+
////////////////////////////////////
|
|
1189
|
+
// END NavigationStrategyManager implementation
|
|
1190
|
+
////////////////////////////////////
|
|
1191
|
+
////////////////////////////////////
|
|
1192
|
+
// StudyContentSource implementation
|
|
1193
|
+
////////////////////////////////////
|
|
1194
|
+
async getNewCards(limit = 99) {
|
|
1195
|
+
const u = await this._getCurrentUser();
|
|
1196
|
+
try {
|
|
1197
|
+
const strategy = await this.surfaceNavigationStrategy();
|
|
1198
|
+
const navigator = await ContentNavigator.create(u, this, strategy);
|
|
1199
|
+
return navigator.getNewCards(limit);
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
|
|
1202
|
+
throw e;
|
|
1203
|
+
}
|
|
1310
1204
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
}
|
|
1314
|
-
async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
|
|
1315
|
-
const db = getCourseDB(courseID);
|
|
1316
|
-
const card = await db.post({
|
|
1317
|
-
course,
|
|
1318
|
-
id_displayable_data,
|
|
1319
|
-
id_view,
|
|
1320
|
-
docType: "CARD" /* CARD */,
|
|
1321
|
-
elo: elo || toCourseElo(990 + Math.round(20 * Math.random())),
|
|
1322
|
-
author
|
|
1323
|
-
});
|
|
1324
|
-
for (const tag of tags) {
|
|
1325
|
-
logger.info(`adding tag: ${tag} to card ${card.id}`);
|
|
1326
|
-
await addTagToCard(courseID, card.id, tag, author, false);
|
|
1327
|
-
}
|
|
1328
|
-
return card;
|
|
1329
|
-
}
|
|
1330
|
-
async function getCredentialledCourseConfig(courseID) {
|
|
1331
|
-
try {
|
|
1332
|
-
const db = getCourseDB(courseID);
|
|
1333
|
-
const ret = await db.get("CourseConfig");
|
|
1334
|
-
ret.courseID = courseID;
|
|
1335
|
-
logger.info(`Returning course config: ${JSON.stringify(ret)}`);
|
|
1336
|
-
return ret;
|
|
1337
|
-
} catch (e) {
|
|
1338
|
-
logger.error(`Error fetching config for ${courseID}:`, e);
|
|
1339
|
-
throw e;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
async function addTagToCard(courseID, cardID, tagID, author, updateELO = true) {
|
|
1343
|
-
const prefixedTagID = getTagID(tagID);
|
|
1344
|
-
const courseDB = getCourseDB(courseID);
|
|
1345
|
-
const courseApi = new CourseDB(courseID, async () => {
|
|
1346
|
-
const dummySyncStrategy = {
|
|
1347
|
-
setupRemoteDB: () => null,
|
|
1348
|
-
startSync: () => {
|
|
1349
|
-
},
|
|
1350
|
-
canCreateAccount: () => false,
|
|
1351
|
-
canAuthenticate: () => false,
|
|
1352
|
-
getCurrentUsername: async () => "DummyUser"
|
|
1353
|
-
};
|
|
1354
|
-
return BaseUser.Dummy(dummySyncStrategy);
|
|
1355
|
-
});
|
|
1356
|
-
try {
|
|
1357
|
-
logger.info(`Applying tag ${tagID} to card ${courseID + "-" + cardID}...`);
|
|
1358
|
-
const tag = await courseDB.get(prefixedTagID);
|
|
1359
|
-
if (!tag.taggedCards.includes(cardID)) {
|
|
1360
|
-
tag.taggedCards.push(cardID);
|
|
1361
|
-
if (updateELO) {
|
|
1205
|
+
async getPendingReviews() {
|
|
1206
|
+
const u = await this._getCurrentUser();
|
|
1362
1207
|
try {
|
|
1363
|
-
const
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
};
|
|
1370
|
-
await updateCardElo(courseID, cardID, elo);
|
|
1371
|
-
} catch (error) {
|
|
1372
|
-
logger.error("Failed to update ELO data for card:", cardID, error);
|
|
1208
|
+
const strategy = await this.surfaceNavigationStrategy();
|
|
1209
|
+
const navigator = await ContentNavigator.create(u, this, strategy);
|
|
1210
|
+
return navigator.getPendingReviews();
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
|
|
1213
|
+
throw e;
|
|
1373
1214
|
}
|
|
1374
1215
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1216
|
+
async getCardsCenteredAtELO(options = {
|
|
1217
|
+
limit: 99,
|
|
1218
|
+
elo: "user"
|
|
1219
|
+
}, filter) {
|
|
1220
|
+
let targetElo;
|
|
1221
|
+
if (options.elo === "user") {
|
|
1222
|
+
const u = await this._getCurrentUser();
|
|
1223
|
+
targetElo = -1;
|
|
1224
|
+
try {
|
|
1225
|
+
const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
|
|
1226
|
+
return c.courseID === this.id;
|
|
1227
|
+
});
|
|
1228
|
+
targetElo = EloToNumber(courseDoc.elo);
|
|
1229
|
+
} catch {
|
|
1230
|
+
targetElo = 1e3;
|
|
1231
|
+
}
|
|
1232
|
+
} else if (options.elo === "random") {
|
|
1233
|
+
const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
|
|
1234
|
+
targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
|
|
1235
|
+
} else {
|
|
1236
|
+
targetElo = options.elo;
|
|
1237
|
+
}
|
|
1238
|
+
let cards = [];
|
|
1239
|
+
let mult = 4;
|
|
1240
|
+
let previousCount = -1;
|
|
1241
|
+
let newCount = 0;
|
|
1242
|
+
while (cards.length < options.limit && newCount !== previousCount) {
|
|
1243
|
+
cards = await this.getCardsByELO(targetElo, mult * options.limit);
|
|
1244
|
+
previousCount = newCount;
|
|
1245
|
+
newCount = cards.length;
|
|
1246
|
+
logger.debug(`Found ${cards.length} elo neighbor cards...`);
|
|
1247
|
+
if (filter) {
|
|
1248
|
+
cards = cards.filter(filter);
|
|
1249
|
+
logger.debug(`Filtered to ${cards.length} cards...`);
|
|
1250
|
+
}
|
|
1251
|
+
mult *= 2;
|
|
1252
|
+
}
|
|
1253
|
+
const selectedCards = [];
|
|
1254
|
+
while (selectedCards.length < options.limit && cards.length > 0) {
|
|
1255
|
+
const index = randIntWeightedTowardZero(cards.length);
|
|
1256
|
+
const card = cards.splice(index, 1)[0];
|
|
1257
|
+
selectedCards.push(card);
|
|
1258
|
+
}
|
|
1259
|
+
return selectedCards.map((c) => {
|
|
1260
|
+
const split = c.split("-");
|
|
1261
|
+
return {
|
|
1262
|
+
courseID: this.id,
|
|
1263
|
+
cardID: split[1],
|
|
1264
|
+
qualifiedID: `${split[0]}-${split[1]}`,
|
|
1265
|
+
contentSourceType: "course",
|
|
1266
|
+
contentSourceID: this.id,
|
|
1267
|
+
status: "new"
|
|
1268
|
+
};
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1400
1272
|
}
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// src/impl/couch/classroomDB.ts
|
|
1276
|
+
import moment from "moment";
|
|
1277
|
+
function getClassroomDB(classID, version) {
|
|
1278
|
+
const dbName = `classdb-${version}-${classID}`;
|
|
1279
|
+
logger.info(`Retrieving classroom db: ${dbName}`);
|
|
1404
1280
|
return new pouchdb_setup_default(
|
|
1405
1281
|
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1406
1282
|
pouchDBincludeCredentialsConfig
|
|
1407
1283
|
);
|
|
1408
1284
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
init_factory();
|
|
1416
|
-
init_courseDB();
|
|
1417
|
-
init_types_legacy();
|
|
1418
|
-
init_common();
|
|
1419
|
-
init_logger();
|
|
1420
|
-
AlreadyTaggedErr = class extends Error {
|
|
1421
|
-
constructor(message) {
|
|
1422
|
-
super(message);
|
|
1423
|
-
this.name = "AlreadyTaggedErr";
|
|
1424
|
-
}
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
// src/impl/couch/courseLookupDB.ts
|
|
1430
|
-
var courseLookupDBTitle, CourseLookup;
|
|
1431
|
-
var init_courseLookupDB = __esm({
|
|
1432
|
-
"src/impl/couch/courseLookupDB.ts"() {
|
|
1285
|
+
async function getClassroomConfig(classID) {
|
|
1286
|
+
return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
|
|
1287
|
+
}
|
|
1288
|
+
var classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
|
|
1289
|
+
var init_classroomDB2 = __esm({
|
|
1290
|
+
"src/impl/couch/classroomDB.ts"() {
|
|
1433
1291
|
"use strict";
|
|
1434
|
-
init_pouchdb_setup();
|
|
1435
1292
|
init_factory();
|
|
1436
1293
|
init_logger();
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
// fetch: (url, opts) => { // Optional: Add for debugging network requests
|
|
1467
|
-
// console.log('PouchDB fetch:', url, opts);
|
|
1468
|
-
// return pouch.fetch(url, opts);
|
|
1469
|
-
// }
|
|
1470
|
-
};
|
|
1471
|
-
if (ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD) {
|
|
1472
|
-
options.auth = {
|
|
1473
|
-
username: ENV.COUCHDB_USERNAME,
|
|
1474
|
-
password: ENV.COUCHDB_PASSWORD
|
|
1475
|
-
};
|
|
1476
|
-
logger.info(`CourseLookup: Connecting to ${dbUrl} with authentication.`);
|
|
1294
|
+
init_pouchdb_setup();
|
|
1295
|
+
init_couch();
|
|
1296
|
+
init_courseDB();
|
|
1297
|
+
classroomLookupDBTitle = "classdb-lookup";
|
|
1298
|
+
CLASSROOM_CONFIG = "ClassroomConfig";
|
|
1299
|
+
ClassroomDBBase = class {
|
|
1300
|
+
_id;
|
|
1301
|
+
_db;
|
|
1302
|
+
_cfg;
|
|
1303
|
+
_initComplete = false;
|
|
1304
|
+
_content_prefix = "content";
|
|
1305
|
+
get _content_searchkeys() {
|
|
1306
|
+
return getStartAndEndKeys(this._content_prefix);
|
|
1307
|
+
}
|
|
1308
|
+
async getAssignedContent() {
|
|
1309
|
+
logger.info(`Getting assigned content...`);
|
|
1310
|
+
const docRows = await this._db.allDocs({
|
|
1311
|
+
startkey: this._content_prefix,
|
|
1312
|
+
endkey: this._content_prefix + `\uFFF0`,
|
|
1313
|
+
include_docs: true
|
|
1314
|
+
});
|
|
1315
|
+
const ret = docRows.rows.map((row) => {
|
|
1316
|
+
return row.doc;
|
|
1317
|
+
});
|
|
1318
|
+
return ret;
|
|
1319
|
+
}
|
|
1320
|
+
getContentId(content) {
|
|
1321
|
+
if (content.type === "tag") {
|
|
1322
|
+
return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
|
|
1477
1323
|
} else {
|
|
1478
|
-
|
|
1324
|
+
return `${this._content_prefix}-${content.courseID}`;
|
|
1479
1325
|
}
|
|
1326
|
+
}
|
|
1327
|
+
get ready() {
|
|
1328
|
+
return this._initComplete;
|
|
1329
|
+
}
|
|
1330
|
+
getConfig() {
|
|
1331
|
+
return this._cfg;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
StudentClassroomDB = class _StudentClassroomDB extends ClassroomDBBase {
|
|
1335
|
+
// private readonly _prefix: string = 'content';
|
|
1336
|
+
userMessages;
|
|
1337
|
+
_user;
|
|
1338
|
+
constructor(classID, user) {
|
|
1339
|
+
super();
|
|
1340
|
+
this._id = classID;
|
|
1341
|
+
this._user = user;
|
|
1342
|
+
}
|
|
1343
|
+
async init() {
|
|
1344
|
+
const dbName = `classdb-student-${this._id}`;
|
|
1345
|
+
this._db = new pouchdb_setup_default(
|
|
1346
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1347
|
+
pouchDBincludeCredentialsConfig
|
|
1348
|
+
);
|
|
1480
1349
|
try {
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1350
|
+
const cfg = await this._db.get(CLASSROOM_CONFIG);
|
|
1351
|
+
this._cfg = cfg;
|
|
1352
|
+
this.userMessages = this._db.changes({
|
|
1353
|
+
since: "now",
|
|
1354
|
+
live: true,
|
|
1355
|
+
include_docs: true
|
|
1356
|
+
});
|
|
1357
|
+
this._initComplete = true;
|
|
1358
|
+
return;
|
|
1359
|
+
} catch (e) {
|
|
1360
|
+
throw new Error(`Error in StudentClassroomDB constructor: ${JSON.stringify(e)}`);
|
|
1490
1361
|
}
|
|
1491
1362
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
*/
|
|
1497
|
-
static async add(courseName) {
|
|
1498
|
-
const resp = await _CourseLookup._db.post({
|
|
1499
|
-
name: courseName
|
|
1500
|
-
});
|
|
1501
|
-
return resp.id;
|
|
1363
|
+
static async factory(classID, user) {
|
|
1364
|
+
const ret = new _StudentClassroomDB(classID, user);
|
|
1365
|
+
await ret.init();
|
|
1366
|
+
return ret;
|
|
1502
1367
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
* @param courseID
|
|
1506
|
-
*/
|
|
1507
|
-
static async delete(courseID) {
|
|
1508
|
-
const doc = await _CourseLookup._db.get(courseID);
|
|
1509
|
-
return await _CourseLookup._db.remove(doc);
|
|
1368
|
+
setChangeFcn(f) {
|
|
1369
|
+
void this.userMessages.on("change", f);
|
|
1510
1370
|
}
|
|
1511
|
-
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1371
|
+
async getPendingReviews() {
|
|
1372
|
+
const u = this._user;
|
|
1373
|
+
return (await u.getPendingReviews()).filter((r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id).map((r) => {
|
|
1374
|
+
return {
|
|
1375
|
+
...r,
|
|
1376
|
+
qualifiedID: `${r.courseId}-${r.cardId}`,
|
|
1377
|
+
courseID: r.courseId,
|
|
1378
|
+
cardID: r.cardId,
|
|
1379
|
+
contentSourceType: "classroom",
|
|
1380
|
+
contentSourceID: this._id,
|
|
1381
|
+
reviewID: r._id,
|
|
1382
|
+
status: "review"
|
|
1383
|
+
};
|
|
1514
1384
|
});
|
|
1515
|
-
return resp.rows.map((row) => row.doc);
|
|
1516
1385
|
}
|
|
1517
|
-
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1386
|
+
async getNewCards() {
|
|
1387
|
+
const activeCards = await this._user.getActiveCards();
|
|
1388
|
+
const now = moment.utc();
|
|
1389
|
+
const assigned = await this.getAssignedContent();
|
|
1390
|
+
const due = assigned.filter((c) => now.isAfter(moment.utc(c.activeOn, REVIEW_TIME_FORMAT)));
|
|
1391
|
+
logger.info(`Due content: ${JSON.stringify(due)}`);
|
|
1392
|
+
let ret = [];
|
|
1393
|
+
for (let i = 0; i < due.length; i++) {
|
|
1394
|
+
const content = due[i];
|
|
1395
|
+
if (content.type === "course") {
|
|
1396
|
+
const db = new CourseDB(content.courseID, async () => this._user);
|
|
1397
|
+
ret = ret.concat(await db.getNewCards());
|
|
1398
|
+
} else if (content.type === "tag") {
|
|
1399
|
+
const tagDoc = await getTag(content.courseID, content.tagID);
|
|
1400
|
+
ret = ret.concat(
|
|
1401
|
+
tagDoc.taggedCards.map((c) => {
|
|
1402
|
+
return {
|
|
1403
|
+
courseID: content.courseID,
|
|
1404
|
+
cardID: c,
|
|
1405
|
+
qualifiedID: `${content.courseID}-${c}`,
|
|
1406
|
+
contentSourceType: "classroom",
|
|
1407
|
+
contentSourceID: this._id,
|
|
1408
|
+
status: "new"
|
|
1409
|
+
};
|
|
1410
|
+
})
|
|
1411
|
+
);
|
|
1412
|
+
} else if (content.type === "card") {
|
|
1413
|
+
ret.push(await getCourseDB2(content.courseID).get(content.cardID));
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
|
|
1417
|
+
return ret.filter((c) => {
|
|
1418
|
+
if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
|
|
1419
|
+
return false;
|
|
1420
|
+
} else {
|
|
1421
|
+
return true;
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1521
1424
|
}
|
|
1522
|
-
|
|
1425
|
+
};
|
|
1426
|
+
TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
|
|
1427
|
+
_stuDb;
|
|
1428
|
+
constructor(classID) {
|
|
1429
|
+
super();
|
|
1430
|
+
this._id = classID;
|
|
1431
|
+
}
|
|
1432
|
+
async init() {
|
|
1433
|
+
const dbName = `classdb-teacher-${this._id}`;
|
|
1434
|
+
const stuDbName = `classdb-student-${this._id}`;
|
|
1435
|
+
this._db = new pouchdb_setup_default(
|
|
1436
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1437
|
+
pouchDBincludeCredentialsConfig
|
|
1438
|
+
);
|
|
1439
|
+
this._stuDb = new pouchdb_setup_default(
|
|
1440
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
|
|
1441
|
+
pouchDBincludeCredentialsConfig
|
|
1442
|
+
);
|
|
1523
1443
|
try {
|
|
1524
|
-
|
|
1444
|
+
return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
|
|
1445
|
+
this._cfg = cfg;
|
|
1446
|
+
this._initComplete = true;
|
|
1447
|
+
}).then(() => {
|
|
1448
|
+
return;
|
|
1449
|
+
});
|
|
1450
|
+
} catch (e) {
|
|
1451
|
+
throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
static async factory(classID) {
|
|
1455
|
+
const ret = new _TeacherClassroomDB(classID);
|
|
1456
|
+
await ret.init();
|
|
1457
|
+
return ret;
|
|
1458
|
+
}
|
|
1459
|
+
async removeContent(content) {
|
|
1460
|
+
const contentID = this.getContentId(content);
|
|
1461
|
+
try {
|
|
1462
|
+
const doc = await this._db.get(contentID);
|
|
1463
|
+
await this._db.remove(doc);
|
|
1464
|
+
void this._db.replicate.to(this._stuDb, {
|
|
1465
|
+
doc_ids: [contentID]
|
|
1466
|
+
});
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
logger.error("Failed to remove content:", contentID, error);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
async assignContent(content) {
|
|
1472
|
+
let put;
|
|
1473
|
+
const id = this.getContentId(content);
|
|
1474
|
+
if (content.type === "tag") {
|
|
1475
|
+
put = await this._db.put({
|
|
1476
|
+
courseID: content.courseID,
|
|
1477
|
+
tagID: content.tagID,
|
|
1478
|
+
type: "tag",
|
|
1479
|
+
_id: id,
|
|
1480
|
+
assignedBy: content.assignedBy,
|
|
1481
|
+
assignedOn: moment.utc(),
|
|
1482
|
+
activeOn: content.activeOn || moment.utc()
|
|
1483
|
+
});
|
|
1484
|
+
} else {
|
|
1485
|
+
put = await this._db.put({
|
|
1486
|
+
courseID: content.courseID,
|
|
1487
|
+
type: "course",
|
|
1488
|
+
_id: id,
|
|
1489
|
+
assignedBy: content.assignedBy,
|
|
1490
|
+
assignedOn: moment.utc(),
|
|
1491
|
+
activeOn: content.activeOn || moment.utc()
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (put.ok) {
|
|
1495
|
+
void this._db.replicate.to(this._stuDb, {
|
|
1496
|
+
doc_ids: [id]
|
|
1497
|
+
});
|
|
1525
1498
|
return true;
|
|
1526
|
-
}
|
|
1527
|
-
logger.info(`Courselookup failed:`, error);
|
|
1499
|
+
} else {
|
|
1528
1500
|
return false;
|
|
1529
1501
|
}
|
|
1530
1502
|
}
|
|
1531
1503
|
};
|
|
1504
|
+
ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
|
|
1505
|
+
skip_setup: true
|
|
1506
|
+
});
|
|
1532
1507
|
}
|
|
1533
1508
|
});
|
|
1534
1509
|
|
|
1535
|
-
// src/core/
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1510
|
+
// src/core/interfaces/contentSource.ts
|
|
1511
|
+
function isReview(item) {
|
|
1512
|
+
const ret = item.status === "review" || item.status === "failed-review" || "reviewID" in item;
|
|
1513
|
+
return ret;
|
|
1514
|
+
}
|
|
1515
|
+
async function getStudySource(source, user) {
|
|
1516
|
+
if (source.type === "classroom") {
|
|
1517
|
+
return await StudentClassroomDB.factory(source.id, user);
|
|
1518
|
+
} else {
|
|
1519
|
+
return getDataLayer().getCourseDB(source.id);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
var init_contentSource = __esm({
|
|
1523
|
+
"src/core/interfaces/contentSource.ts"() {
|
|
1524
|
+
"use strict";
|
|
1525
|
+
init_factory();
|
|
1526
|
+
init_classroomDB2();
|
|
1527
|
+
}
|
|
1539
1528
|
});
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1529
|
+
|
|
1530
|
+
// src/core/interfaces/courseDB.ts
|
|
1531
|
+
var init_courseDB2 = __esm({
|
|
1532
|
+
"src/core/interfaces/courseDB.ts"() {
|
|
1543
1533
|
"use strict";
|
|
1544
|
-
init_navigators();
|
|
1545
|
-
ELONavigator = class extends ContentNavigator {
|
|
1546
|
-
user;
|
|
1547
|
-
course;
|
|
1548
|
-
constructor(user, course) {
|
|
1549
|
-
super();
|
|
1550
|
-
this.user = user;
|
|
1551
|
-
this.course = course;
|
|
1552
|
-
}
|
|
1553
|
-
async getPendingReviews() {
|
|
1554
|
-
const reviews = await this.user.getPendingReviews(this.course.getCourseID());
|
|
1555
|
-
const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
|
|
1556
|
-
const ratedReviews = reviews.map((r, i) => {
|
|
1557
|
-
const ratedR = {
|
|
1558
|
-
...r,
|
|
1559
|
-
...elo[i]
|
|
1560
|
-
};
|
|
1561
|
-
return ratedR;
|
|
1562
|
-
});
|
|
1563
|
-
ratedReviews.sort((a, b) => {
|
|
1564
|
-
return a.global.score - b.global.score;
|
|
1565
|
-
});
|
|
1566
|
-
return ratedReviews.map((r) => {
|
|
1567
|
-
return {
|
|
1568
|
-
...r,
|
|
1569
|
-
contentSourceType: "course",
|
|
1570
|
-
contentSourceID: this.course.getCourseID(),
|
|
1571
|
-
cardID: r.cardId,
|
|
1572
|
-
courseID: r.courseId,
|
|
1573
|
-
qualifiedID: `${r.courseId}-${r.cardId}`,
|
|
1574
|
-
reviewID: r._id,
|
|
1575
|
-
status: "review"
|
|
1576
|
-
};
|
|
1577
|
-
});
|
|
1578
|
-
}
|
|
1579
|
-
async getNewCards(limit = 99) {
|
|
1580
|
-
const activeCards = await this.user.getActiveCards();
|
|
1581
|
-
return (await this.course.getCardsCenteredAtELO({ limit, elo: "user" }, (c) => {
|
|
1582
|
-
if (activeCards.some((ac) => c.includes(ac))) {
|
|
1583
|
-
return false;
|
|
1584
|
-
} else {
|
|
1585
|
-
return true;
|
|
1586
|
-
}
|
|
1587
|
-
})).map((c) => {
|
|
1588
|
-
return {
|
|
1589
|
-
...c,
|
|
1590
|
-
status: "new"
|
|
1591
|
-
};
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
};
|
|
1595
1534
|
}
|
|
1596
1535
|
});
|
|
1597
1536
|
|
|
1598
|
-
//
|
|
1599
|
-
var
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
globImport = __glob({
|
|
1603
|
-
"./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1604
|
-
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
1605
|
-
});
|
|
1537
|
+
// src/core/interfaces/dataLayerProvider.ts
|
|
1538
|
+
var init_dataLayerProvider = __esm({
|
|
1539
|
+
"src/core/interfaces/dataLayerProvider.ts"() {
|
|
1540
|
+
"use strict";
|
|
1606
1541
|
}
|
|
1607
1542
|
});
|
|
1608
1543
|
|
|
1609
|
-
// src/core/
|
|
1610
|
-
var
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1544
|
+
// src/core/interfaces/userDB.ts
|
|
1545
|
+
var init_userDB = __esm({
|
|
1546
|
+
"src/core/interfaces/userDB.ts"() {
|
|
1547
|
+
"use strict";
|
|
1548
|
+
}
|
|
1614
1549
|
});
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1550
|
+
|
|
1551
|
+
// src/core/interfaces/index.ts
|
|
1552
|
+
var init_interfaces = __esm({
|
|
1553
|
+
"src/core/interfaces/index.ts"() {
|
|
1618
1554
|
"use strict";
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
ContentNavigator = class {
|
|
1626
|
-
/**
|
|
1627
|
-
*
|
|
1628
|
-
* @param user
|
|
1629
|
-
* @param strategyData
|
|
1630
|
-
* @returns the runtime object used to steer a study session.
|
|
1631
|
-
*/
|
|
1632
|
-
static async create(user, course, strategyData) {
|
|
1633
|
-
const implementingClass = strategyData.implementingClass;
|
|
1634
|
-
let NavigatorImpl;
|
|
1635
|
-
const variations = ["", ".js", ".ts"];
|
|
1636
|
-
for (const ext of variations) {
|
|
1637
|
-
try {
|
|
1638
|
-
const module = await globImport(`./${implementingClass}${ext}`);
|
|
1639
|
-
NavigatorImpl = module.default;
|
|
1640
|
-
break;
|
|
1641
|
-
} catch (e) {
|
|
1642
|
-
logger.debug(`Failed to load with extension ${ext}:`, e);
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
if (!NavigatorImpl) {
|
|
1646
|
-
throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
|
|
1647
|
-
}
|
|
1648
|
-
return new NavigatorImpl(user, course, strategyData);
|
|
1649
|
-
}
|
|
1650
|
-
};
|
|
1555
|
+
init_adminDB();
|
|
1556
|
+
init_classroomDB();
|
|
1557
|
+
init_contentSource();
|
|
1558
|
+
init_courseDB2();
|
|
1559
|
+
init_dataLayerProvider();
|
|
1560
|
+
init_userDB();
|
|
1651
1561
|
}
|
|
1652
1562
|
});
|
|
1653
1563
|
|
|
1654
|
-
// src/
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1664
|
-
async function getCourseDataShapes(courseID) {
|
|
1665
|
-
const cfg = await getCredentialledCourseConfig(courseID);
|
|
1666
|
-
return cfg.dataShapes;
|
|
1667
|
-
}
|
|
1668
|
-
async function getCredentialledDataShapes(courseID) {
|
|
1669
|
-
const cfg = await getCredentialledCourseConfig(courseID);
|
|
1670
|
-
return cfg.dataShapes;
|
|
1671
|
-
}
|
|
1672
|
-
async function getCourseQuestionTypes(courseID) {
|
|
1673
|
-
const cfg = await getCredentialledCourseConfig(courseID);
|
|
1674
|
-
return cfg.questionTypes;
|
|
1564
|
+
// src/core/types/user.ts
|
|
1565
|
+
var init_user = __esm({
|
|
1566
|
+
"src/core/types/user.ts"() {
|
|
1567
|
+
"use strict";
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
// src/core/util/index.ts
|
|
1572
|
+
function getCardHistoryID(courseID, cardID) {
|
|
1573
|
+
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
1675
1574
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1575
|
+
var init_util = __esm({
|
|
1576
|
+
"src/core/util/index.ts"() {
|
|
1577
|
+
"use strict";
|
|
1578
|
+
init_types_legacy();
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
// src/core/bulkImport/cardProcessor.ts
|
|
1583
|
+
import { Status as Status2 } from "@vue-skuilder/common";
|
|
1584
|
+
var init_cardProcessor = __esm({
|
|
1585
|
+
"src/core/bulkImport/cardProcessor.ts"() {
|
|
1586
|
+
"use strict";
|
|
1587
|
+
init_logger();
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
// src/core/bulkImport/types.ts
|
|
1592
|
+
var init_types = __esm({
|
|
1593
|
+
"src/core/bulkImport/types.ts"() {
|
|
1594
|
+
"use strict";
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// src/core/bulkImport/index.ts
|
|
1599
|
+
var init_bulkImport = __esm({
|
|
1600
|
+
"src/core/bulkImport/index.ts"() {
|
|
1601
|
+
"use strict";
|
|
1602
|
+
init_cardProcessor();
|
|
1603
|
+
init_types();
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
// src/core/index.ts
|
|
1608
|
+
var init_core = __esm({
|
|
1609
|
+
"src/core/index.ts"() {
|
|
1610
|
+
"use strict";
|
|
1611
|
+
init_interfaces();
|
|
1612
|
+
init_types_legacy();
|
|
1613
|
+
init_user();
|
|
1614
|
+
init_Loggable();
|
|
1615
|
+
init_util();
|
|
1616
|
+
init_navigators();
|
|
1617
|
+
init_bulkImport();
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
// src/util/tuiLogger.ts
|
|
1622
|
+
var init_tuiLogger = __esm({
|
|
1623
|
+
"src/util/tuiLogger.ts"() {
|
|
1624
|
+
"use strict";
|
|
1625
|
+
init_dataDirectory();
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
// src/util/dataDirectory.ts
|
|
1630
|
+
import * as path from "path";
|
|
1631
|
+
import * as os from "os";
|
|
1632
|
+
function getAppDataDirectory() {
|
|
1633
|
+
return path.join(os.homedir(), ".tuilder");
|
|
1686
1634
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const courseDB = getCourseDB2(courseID);
|
|
1690
|
-
const doc = await courseDB.get("TAG" /* TAG */.valueOf() + "-" + tagName);
|
|
1691
|
-
const resp = await courseDB.remove(doc);
|
|
1692
|
-
return resp;
|
|
1635
|
+
function getDbPath(dbName) {
|
|
1636
|
+
return path.join(getAppDataDirectory(), dbName);
|
|
1693
1637
|
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1638
|
+
var init_dataDirectory = __esm({
|
|
1639
|
+
"src/util/dataDirectory.ts"() {
|
|
1640
|
+
"use strict";
|
|
1641
|
+
init_tuiLogger();
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// src/impl/common/userDBHelpers.ts
|
|
1646
|
+
import moment2 from "moment";
|
|
1647
|
+
function hexEncode(str) {
|
|
1648
|
+
let hex;
|
|
1649
|
+
let returnStr = "";
|
|
1650
|
+
for (let i = 0; i < str.length; i++) {
|
|
1651
|
+
hex = str.charCodeAt(i).toString(16);
|
|
1652
|
+
returnStr += ("000" + hex).slice(3);
|
|
1653
|
+
}
|
|
1654
|
+
return returnStr;
|
|
1709
1655
|
}
|
|
1710
|
-
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
}
|
|
1656
|
+
function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
1657
|
+
const options = {
|
|
1658
|
+
startkey: prefix,
|
|
1659
|
+
endkey: prefix + "\uFFF0",
|
|
1660
|
+
include_docs: true
|
|
1661
|
+
};
|
|
1662
|
+
if (opts) {
|
|
1663
|
+
Object.assign(options, opts);
|
|
1664
|
+
}
|
|
1665
|
+
return db.allDocs(options);
|
|
1716
1666
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1667
|
+
function getStartAndEndKeys2(key) {
|
|
1668
|
+
return {
|
|
1669
|
+
startkey: key,
|
|
1670
|
+
endkey: key + "\uFFF0"
|
|
1671
|
+
};
|
|
1721
1672
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
const
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
return
|
|
1673
|
+
function updateGuestAccountExpirationDate(guestDB) {
|
|
1674
|
+
const currentTime = moment2.utc();
|
|
1675
|
+
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
1676
|
+
const expiryDocID2 = "GuestAccountExpirationDate";
|
|
1677
|
+
void guestDB.get(expiryDocID2).then((doc) => {
|
|
1678
|
+
return guestDB.put({
|
|
1679
|
+
_id: expiryDocID2,
|
|
1680
|
+
_rev: doc._rev,
|
|
1681
|
+
date: expirationDate
|
|
1682
|
+
});
|
|
1683
|
+
}).catch(() => {
|
|
1684
|
+
return guestDB.put({
|
|
1685
|
+
_id: expiryDocID2,
|
|
1686
|
+
date: expirationDate
|
|
1687
|
+
});
|
|
1728
1688
|
});
|
|
1729
|
-
return courseDB.put(tag);
|
|
1730
1689
|
}
|
|
1731
|
-
function
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
return [];
|
|
1690
|
+
function getLocalUserDB(username) {
|
|
1691
|
+
const dbName = `userdb-${username}`;
|
|
1692
|
+
if (typeof window === "undefined") {
|
|
1693
|
+
return new pouchdb_setup_default(getDbPath(dbName), {});
|
|
1736
1694
|
} else {
|
|
1737
|
-
|
|
1738
|
-
const parent = split.join(">");
|
|
1739
|
-
return [parent].concat(getAncestorTagIDs(courseID, parent));
|
|
1695
|
+
return new pouchdb_setup_default(dbName, {});
|
|
1740
1696
|
}
|
|
1741
1697
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1698
|
+
function scheduleCardReviewLocal(userDB, review) {
|
|
1699
|
+
const now = moment2.utc();
|
|
1700
|
+
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
1701
|
+
void userDB.put({
|
|
1702
|
+
_id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT2),
|
|
1703
|
+
cardId: review.card_id,
|
|
1704
|
+
reviewTime: review.time.toISOString(),
|
|
1705
|
+
courseId: review.course_id,
|
|
1706
|
+
scheduledAt: now.toISOString(),
|
|
1707
|
+
scheduledFor: review.scheduledFor,
|
|
1708
|
+
schedulingAgentId: review.schedulingAgentId
|
|
1709
|
+
});
|
|
1744
1710
|
}
|
|
1745
|
-
async function
|
|
1746
|
-
const
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1711
|
+
async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
|
|
1712
|
+
const reviewDoc = await userDB.get(reviewDocID);
|
|
1713
|
+
userDB.remove(reviewDoc).then((res) => {
|
|
1714
|
+
if (res.ok) {
|
|
1715
|
+
log2(`Removed Review Doc: ${reviewDocID}`);
|
|
1716
|
+
}
|
|
1717
|
+
}).catch((err) => {
|
|
1718
|
+
log2(`Failed to remove Review Doc: ${reviewDocID},
|
|
1719
|
+
${JSON.stringify(err)}`);
|
|
1751
1720
|
});
|
|
1752
|
-
return result;
|
|
1753
1721
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1722
|
+
var REVIEW_TIME_FORMAT2, log2;
|
|
1723
|
+
var init_userDBHelpers = __esm({
|
|
1724
|
+
"src/impl/common/userDBHelpers.ts"() {
|
|
1725
|
+
"use strict";
|
|
1726
|
+
init_core();
|
|
1727
|
+
init_logger();
|
|
1728
|
+
init_pouchdb_setup();
|
|
1729
|
+
init_dataDirectory();
|
|
1730
|
+
REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
1731
|
+
log2 = (s) => {
|
|
1732
|
+
logger.info(s);
|
|
1733
|
+
};
|
|
1761
1734
|
}
|
|
1762
|
-
}
|
|
1763
|
-
async function updateCredentialledCourseConfig(courseID, config) {
|
|
1764
|
-
logger.debug(`Updating course config:
|
|
1735
|
+
});
|
|
1765
1736
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
...config,
|
|
1772
|
-
_rev: old._rev
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
function isSuccessRow(row) {
|
|
1776
|
-
return "doc" in row && row.doc !== null && row.doc !== void 0;
|
|
1777
|
-
}
|
|
1778
|
-
var CoursesDB, CourseDB;
|
|
1779
|
-
var init_courseDB = __esm({
|
|
1780
|
-
"src/impl/couch/courseDB.ts"() {
|
|
1737
|
+
// src/impl/couch/user-course-relDB.ts
|
|
1738
|
+
import moment3 from "moment";
|
|
1739
|
+
var UsrCrsData;
|
|
1740
|
+
var init_user_course_relDB = __esm({
|
|
1741
|
+
"src/impl/couch/user-course-relDB.ts"() {
|
|
1781
1742
|
"use strict";
|
|
1782
|
-
init_couch();
|
|
1783
|
-
init_updateQueue();
|
|
1784
|
-
init_types_legacy();
|
|
1785
1743
|
init_logger();
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
constructor(courseIDs) {
|
|
1793
|
-
if (courseIDs && courseIDs.length > 0) {
|
|
1794
|
-
this._courseIDs = courseIDs;
|
|
1795
|
-
} else {
|
|
1796
|
-
this._courseIDs = void 0;
|
|
1797
|
-
}
|
|
1744
|
+
UsrCrsData = class {
|
|
1745
|
+
user;
|
|
1746
|
+
_courseId;
|
|
1747
|
+
constructor(user, courseId) {
|
|
1748
|
+
this.user = user;
|
|
1749
|
+
this._courseId = courseId;
|
|
1798
1750
|
}
|
|
1799
|
-
async
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
if (this._courseIDs) {
|
|
1803
|
-
crsList = crsList.filter((c) => this._courseIDs.includes(c._id));
|
|
1804
|
-
}
|
|
1805
|
-
logger.debug(`AllCourses.filtered: ${crsList.map((c) => c.name + ", " + c._id + "\n ")}`);
|
|
1806
|
-
const cfgs = await Promise.all(
|
|
1807
|
-
crsList.map(async (c) => {
|
|
1808
|
-
try {
|
|
1809
|
-
const cfg = await getCredentialledCourseConfig(c._id);
|
|
1810
|
-
logger.debug(`Found cfg: ${JSON.stringify(cfg)}`);
|
|
1811
|
-
return cfg;
|
|
1812
|
-
} catch (e) {
|
|
1813
|
-
logger.warn(`Error fetching cfg for course ${c.name}, ${c._id}: ${e}`);
|
|
1814
|
-
return void 0;
|
|
1815
|
-
}
|
|
1816
|
-
})
|
|
1817
|
-
);
|
|
1818
|
-
return cfgs.filter((c) => !!c);
|
|
1751
|
+
async getReviewsForcast(daysCount) {
|
|
1752
|
+
const time = moment3.utc().add(daysCount, "days");
|
|
1753
|
+
return this.getReviewstoDate(time);
|
|
1819
1754
|
}
|
|
1820
|
-
async
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1755
|
+
async getPendingReviews() {
|
|
1756
|
+
const now = moment3.utc();
|
|
1757
|
+
return this.getReviewstoDate(now);
|
|
1758
|
+
}
|
|
1759
|
+
async getScheduledReviewCount() {
|
|
1760
|
+
return (await this.getPendingReviews()).length;
|
|
1761
|
+
}
|
|
1762
|
+
async getCourseSettings() {
|
|
1763
|
+
const regDoc = await this.user.getCourseRegistrationsDoc();
|
|
1764
|
+
const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
|
|
1765
|
+
if (crsDoc && crsDoc.settings) {
|
|
1766
|
+
return crsDoc.settings;
|
|
1827
1767
|
} else {
|
|
1828
|
-
|
|
1768
|
+
logger.warn(`no settings found during lookup on course ${this._courseId}`);
|
|
1769
|
+
return {};
|
|
1829
1770
|
}
|
|
1830
1771
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1772
|
+
updateCourseSettings(updates) {
|
|
1773
|
+
if ("updateCourseSettings" in this.user) {
|
|
1774
|
+
void this.user.updateCourseSettings(this._courseId, updates);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
async getReviewstoDate(targetDate) {
|
|
1778
|
+
const allReviews = await this.user.getPendingReviews(this._courseId);
|
|
1779
|
+
logger.debug(
|
|
1780
|
+
`Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
|
|
1781
|
+
);
|
|
1782
|
+
return allReviews.filter((review) => {
|
|
1783
|
+
const reviewTime = moment3.utc(review.reviewTime);
|
|
1784
|
+
return targetDate.isAfter(reviewTime);
|
|
1785
|
+
});
|
|
1833
1786
|
}
|
|
1834
1787
|
};
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// src/impl/common/BaseUserDB.ts
|
|
1792
|
+
import { Status as Status3 } from "@vue-skuilder/common";
|
|
1793
|
+
import moment4 from "moment";
|
|
1794
|
+
async function getOrCreateClassroomRegistrationsDoc(user) {
|
|
1795
|
+
let ret;
|
|
1796
|
+
try {
|
|
1797
|
+
ret = await getLocalUserDB(user).get(userClassroomsDoc);
|
|
1798
|
+
} catch (e) {
|
|
1799
|
+
const err = e;
|
|
1800
|
+
if (err.status === 404) {
|
|
1801
|
+
await getLocalUserDB(user).put({
|
|
1802
|
+
_id: userClassroomsDoc,
|
|
1803
|
+
registrations: []
|
|
1804
|
+
});
|
|
1805
|
+
ret = await getOrCreateClassroomRegistrationsDoc(user);
|
|
1806
|
+
} else {
|
|
1807
|
+
const errorDetails = {
|
|
1808
|
+
name: err.name,
|
|
1809
|
+
status: err.status,
|
|
1810
|
+
message: err.message,
|
|
1811
|
+
reason: err.reason,
|
|
1812
|
+
error: err.error
|
|
1813
|
+
};
|
|
1814
|
+
logger.error(
|
|
1815
|
+
"Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
|
|
1816
|
+
errorDetails
|
|
1817
|
+
);
|
|
1818
|
+
throw new Error(
|
|
1819
|
+
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return ret;
|
|
1824
|
+
}
|
|
1825
|
+
async function getOrCreateCourseRegistrationsDoc(user) {
|
|
1826
|
+
let ret;
|
|
1827
|
+
try {
|
|
1828
|
+
ret = await getLocalUserDB(user).get(userCoursesDoc);
|
|
1829
|
+
} catch (e) {
|
|
1830
|
+
const err = e;
|
|
1831
|
+
if (err.status === 404) {
|
|
1832
|
+
await getLocalUserDB(user).put({
|
|
1833
|
+
_id: userCoursesDoc,
|
|
1834
|
+
courses: [],
|
|
1835
|
+
studyWeight: {}
|
|
1836
|
+
});
|
|
1837
|
+
ret = await getOrCreateCourseRegistrationsDoc(user);
|
|
1838
|
+
} else {
|
|
1839
|
+
throw new Error(
|
|
1840
|
+
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return ret;
|
|
1845
|
+
}
|
|
1846
|
+
async function updateUserElo(user, course_id, elo) {
|
|
1847
|
+
const regDoc = await getOrCreateCourseRegistrationsDoc(user);
|
|
1848
|
+
const course = regDoc.courses.find((c) => c.courseID === course_id);
|
|
1849
|
+
course.elo = elo;
|
|
1850
|
+
return getLocalUserDB(user).put(regDoc);
|
|
1851
|
+
}
|
|
1852
|
+
async function registerUserForClassroom(user, classID, registerAs) {
|
|
1853
|
+
log3(`Registering user: ${user} in course: ${classID}`);
|
|
1854
|
+
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
1855
|
+
const regItem = {
|
|
1856
|
+
classID,
|
|
1857
|
+
registeredAs: registerAs
|
|
1858
|
+
};
|
|
1859
|
+
if (doc.registrations.filter((reg) => {
|
|
1860
|
+
return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
|
|
1861
|
+
}).length === 0) {
|
|
1862
|
+
doc.registrations.push(regItem);
|
|
1863
|
+
} else {
|
|
1864
|
+
log3(`User ${user} is already registered for class ${classID}`);
|
|
1865
|
+
}
|
|
1866
|
+
return getLocalUserDB(user).put(doc);
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
async function dropUserFromClassroom(user, classID) {
|
|
1870
|
+
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
1871
|
+
let index = -1;
|
|
1872
|
+
for (let i = 0; i < doc.registrations.length; i++) {
|
|
1873
|
+
if (doc.registrations[i].classID === classID) {
|
|
1874
|
+
index = i;
|
|
1848
1875
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1876
|
+
}
|
|
1877
|
+
if (index !== -1) {
|
|
1878
|
+
doc.registrations.splice(index, 1);
|
|
1879
|
+
}
|
|
1880
|
+
return getLocalUserDB(user).put(doc);
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
async function getUserClassrooms(user) {
|
|
1884
|
+
return getOrCreateClassroomRegistrationsDoc(user);
|
|
1885
|
+
}
|
|
1886
|
+
var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
1887
|
+
var init_BaseUserDB = __esm({
|
|
1888
|
+
"src/impl/common/BaseUserDB.ts"() {
|
|
1889
|
+
"use strict";
|
|
1890
|
+
init_core();
|
|
1891
|
+
init_util();
|
|
1892
|
+
init_types_legacy();
|
|
1893
|
+
init_logger();
|
|
1894
|
+
init_userDBHelpers();
|
|
1895
|
+
init_updateQueue();
|
|
1896
|
+
init_user_course_relDB();
|
|
1897
|
+
init_couch();
|
|
1898
|
+
log3 = (s) => {
|
|
1899
|
+
logger.info(s);
|
|
1900
|
+
};
|
|
1901
|
+
BaseUser = class _BaseUser {
|
|
1902
|
+
static _instance;
|
|
1903
|
+
static _initialized = false;
|
|
1904
|
+
static Dummy(syncStrategy) {
|
|
1905
|
+
return new _BaseUser("Me", syncStrategy);
|
|
1851
1906
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
};
|
|
1907
|
+
static DOC_IDS = {
|
|
1908
|
+
CONFIG: "CONFIG",
|
|
1909
|
+
COURSE_REGISTRATIONS: "CourseRegistrations",
|
|
1910
|
+
CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
|
|
1911
|
+
};
|
|
1912
|
+
// private email: string;
|
|
1913
|
+
_username;
|
|
1914
|
+
syncStrategy;
|
|
1915
|
+
getUsername() {
|
|
1916
|
+
return this._username;
|
|
1863
1917
|
}
|
|
1864
|
-
|
|
1865
|
-
return
|
|
1866
|
-
limit
|
|
1867
|
-
})).rows.map((r) => {
|
|
1868
|
-
const ret = {
|
|
1869
|
-
courseId: this.id,
|
|
1870
|
-
cardId: r.id,
|
|
1871
|
-
count: r.key,
|
|
1872
|
-
elo: r.value
|
|
1873
|
-
};
|
|
1874
|
-
return ret;
|
|
1875
|
-
});
|
|
1918
|
+
isLoggedIn() {
|
|
1919
|
+
return !this._username.startsWith(GuestUsername);
|
|
1876
1920
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
high: Number.MIN_SAFE_INTEGER,
|
|
1880
|
-
limit: 25,
|
|
1881
|
-
page: 0
|
|
1882
|
-
}) {
|
|
1883
|
-
return (await this.db.query("elo", {
|
|
1884
|
-
startkey: options.low,
|
|
1885
|
-
endkey: options.high,
|
|
1886
|
-
limit: options.limit,
|
|
1887
|
-
skip: options.limit * options.page
|
|
1888
|
-
})).rows.map((r) => {
|
|
1889
|
-
return `${this.id}-${r.id}-${r.key}`;
|
|
1890
|
-
});
|
|
1921
|
+
remote() {
|
|
1922
|
+
return this.remoteDB;
|
|
1891
1923
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1924
|
+
localDB;
|
|
1925
|
+
remoteDB;
|
|
1926
|
+
writeDB;
|
|
1927
|
+
// Database to use for write operations (local-first approach)
|
|
1928
|
+
updateQueue;
|
|
1929
|
+
async createAccount(username, password) {
|
|
1930
|
+
if (!this.syncStrategy.canCreateAccount()) {
|
|
1931
|
+
throw new Error("Account creation not supported by current sync strategy");
|
|
1932
|
+
}
|
|
1933
|
+
if (!this._username.startsWith(GuestUsername)) {
|
|
1934
|
+
throw new Error(
|
|
1935
|
+
`Cannot create a new account while logged in:
|
|
1936
|
+
Currently logged-in as ${this._username}.`
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
const result = await this.syncStrategy.createAccount(username, password);
|
|
1940
|
+
if (result.status === Status3.ok) {
|
|
1941
|
+
log3(`Account created successfully, updating username to ${username}`);
|
|
1942
|
+
this._username = username;
|
|
1943
|
+
try {
|
|
1944
|
+
localStorage.removeItem("dbUUID");
|
|
1945
|
+
} catch (e) {
|
|
1946
|
+
logger.warn("localStorage not available (Node.js environment):", e);
|
|
1909
1947
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
}
|
|
1913
|
-
/**
|
|
1914
|
-
* Returns the lowest and highest `global` ELO ratings in the course
|
|
1915
|
-
*/
|
|
1916
|
-
async getELOBounds() {
|
|
1917
|
-
const [low, high] = await Promise.all([
|
|
1918
|
-
(await this.db.query("elo", {
|
|
1919
|
-
startkey: 0,
|
|
1920
|
-
limit: 1,
|
|
1921
|
-
include_docs: false
|
|
1922
|
-
})).rows[0].key,
|
|
1923
|
-
(await this.db.query("elo", {
|
|
1924
|
-
limit: 1,
|
|
1925
|
-
descending: true,
|
|
1926
|
-
startkey: 1e5
|
|
1927
|
-
})).rows[0].key
|
|
1928
|
-
]);
|
|
1948
|
+
await this.init();
|
|
1949
|
+
}
|
|
1929
1950
|
return {
|
|
1930
|
-
|
|
1931
|
-
|
|
1951
|
+
status: result.status,
|
|
1952
|
+
error: result.error || ""
|
|
1932
1953
|
};
|
|
1933
1954
|
}
|
|
1934
|
-
async
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
1955
|
+
async login(username, password) {
|
|
1956
|
+
if (!this.syncStrategy.canAuthenticate()) {
|
|
1957
|
+
throw new Error("Authentication not supported by current sync strategy");
|
|
1938
1958
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
const cards = await this.db.allDocs({
|
|
1944
|
-
keys: id,
|
|
1945
|
-
include_docs: true
|
|
1946
|
-
});
|
|
1947
|
-
const ret = {};
|
|
1948
|
-
cards.rows.forEach((r) => {
|
|
1949
|
-
if (isSuccessRow(r)) {
|
|
1950
|
-
ret[r.id] = r.doc.id_displayable_data;
|
|
1951
|
-
}
|
|
1952
|
-
});
|
|
1953
|
-
await Promise.all(
|
|
1954
|
-
cards.rows.map((r) => {
|
|
1955
|
-
return async () => {
|
|
1956
|
-
if (isSuccessRow(r)) {
|
|
1957
|
-
ret[r.id] = r.doc.id_displayable_data;
|
|
1958
|
-
}
|
|
1959
|
-
};
|
|
1960
|
-
})
|
|
1961
|
-
);
|
|
1962
|
-
return ret;
|
|
1963
|
-
}
|
|
1964
|
-
async getCardsByELO(elo, cardLimit) {
|
|
1965
|
-
elo = parseInt(elo);
|
|
1966
|
-
const limit = cardLimit ? cardLimit : 25;
|
|
1967
|
-
const below = await this.db.query("elo", {
|
|
1968
|
-
limit: Math.ceil(limit / 2),
|
|
1969
|
-
startkey: elo,
|
|
1970
|
-
descending: true
|
|
1971
|
-
});
|
|
1972
|
-
const aboveLimit = limit - below.rows.length;
|
|
1973
|
-
const above = await this.db.query("elo", {
|
|
1974
|
-
limit: aboveLimit,
|
|
1975
|
-
startkey: elo + 1
|
|
1976
|
-
});
|
|
1977
|
-
let cards = below.rows;
|
|
1978
|
-
cards = cards.concat(above.rows);
|
|
1979
|
-
const ret = cards.sort((a, b) => {
|
|
1980
|
-
const s = Math.abs(a.key - elo) - Math.abs(b.key - elo);
|
|
1981
|
-
if (s === 0) {
|
|
1982
|
-
return Math.random() - 0.5;
|
|
1983
|
-
} else {
|
|
1984
|
-
return s;
|
|
1959
|
+
if (!this._username.startsWith(GuestUsername) && this._username != username) {
|
|
1960
|
+
if (this._username != username) {
|
|
1961
|
+
throw new Error(`Cannot change accounts while logged in.
|
|
1962
|
+
Log out of account ${this.getUsername()} before logging in as ${username}.`);
|
|
1985
1963
|
}
|
|
1986
|
-
|
|
1987
|
-
const str = `below:
|
|
1988
|
-
${below.rows.map((r) => ` ${r.id}-${r.key}
|
|
1989
|
-
`)}
|
|
1990
|
-
|
|
1991
|
-
above:
|
|
1992
|
-
${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
1993
|
-
`)}`;
|
|
1994
|
-
logger.debug(`Getting ${limit} cards centered around elo: ${elo}:
|
|
1995
|
-
|
|
1996
|
-
` + str);
|
|
1997
|
-
return ret;
|
|
1998
|
-
}
|
|
1999
|
-
async getCourseConfig() {
|
|
2000
|
-
const ret = await getCredentialledCourseConfig(this.id);
|
|
2001
|
-
if (ret) {
|
|
2002
|
-
return ret;
|
|
2003
|
-
} else {
|
|
2004
|
-
throw new Error(`Course config not found for course ID: ${this.id}`);
|
|
1964
|
+
logger.warn(`User ${this._username} is already logged in, but executing login again.`);
|
|
2005
1965
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
1966
|
+
const loginResult = await this.syncStrategy.authenticate(username, password);
|
|
1967
|
+
if (loginResult.ok) {
|
|
1968
|
+
log3(`Logged in as ${username}`);
|
|
1969
|
+
this._username = username;
|
|
1970
|
+
try {
|
|
1971
|
+
localStorage.removeItem("dbUUID");
|
|
1972
|
+
} catch (e) {
|
|
1973
|
+
logger.warn("localStorage not available (Node.js environment):", e);
|
|
1974
|
+
}
|
|
1975
|
+
await this.init();
|
|
2014
1976
|
}
|
|
1977
|
+
return loginResult;
|
|
2015
1978
|
}
|
|
2016
|
-
async
|
|
2017
|
-
if (
|
|
2018
|
-
|
|
1979
|
+
async resetUserData() {
|
|
1980
|
+
if (this.syncStrategy.canAuthenticate()) {
|
|
1981
|
+
return {
|
|
1982
|
+
status: Status3.error,
|
|
1983
|
+
error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
|
|
1984
|
+
};
|
|
2019
1985
|
}
|
|
2020
1986
|
try {
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
1987
|
+
const localDB = getLocalUserDB(this._username);
|
|
1988
|
+
const allDocs = await localDB.allDocs({ include_docs: false });
|
|
1989
|
+
const docsToDelete = allDocs.rows.filter((row) => {
|
|
1990
|
+
const id = row.id;
|
|
1991
|
+
return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
|
|
1992
|
+
id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
|
|
1993
|
+
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
1994
|
+
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
1995
|
+
id === _BaseUser.DOC_IDS.CONFIG;
|
|
1996
|
+
}).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
|
|
1997
|
+
if (docsToDelete.length > 0) {
|
|
1998
|
+
await localDB.bulkDocs(docsToDelete);
|
|
1999
|
+
}
|
|
2000
|
+
await this.init();
|
|
2001
|
+
return { status: Status3.ok };
|
|
2027
2002
|
} catch (error) {
|
|
2028
|
-
logger.error(
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
const ret = await getAppliedTags(this.id, cardId);
|
|
2034
|
-
if (ret) {
|
|
2035
|
-
return ret;
|
|
2036
|
-
} else {
|
|
2037
|
-
throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
|
|
2003
|
+
logger.error("Failed to reset user data:", error);
|
|
2004
|
+
return {
|
|
2005
|
+
status: Status3.error,
|
|
2006
|
+
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
2007
|
+
};
|
|
2038
2008
|
}
|
|
2039
2009
|
}
|
|
2040
|
-
async
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2046
|
-
async createTag(name, author) {
|
|
2047
|
-
return await createTag(this.id, name, author);
|
|
2048
|
-
}
|
|
2049
|
-
async getTag(tagId) {
|
|
2050
|
-
return await getTag(this.id, tagId);
|
|
2051
|
-
}
|
|
2052
|
-
async updateTag(tag) {
|
|
2053
|
-
if (tag.course !== this.id) {
|
|
2054
|
-
throw new Error(`Tag ${JSON.stringify(tag)} does not belong to course ${this.id}`);
|
|
2010
|
+
async logout() {
|
|
2011
|
+
if (!this.syncStrategy.canAuthenticate()) {
|
|
2012
|
+
this._username = await this.syncStrategy.getCurrentUsername();
|
|
2013
|
+
await this.init();
|
|
2014
|
+
return { ok: true };
|
|
2055
2015
|
}
|
|
2056
|
-
|
|
2016
|
+
const ret = await this.syncStrategy.logout();
|
|
2017
|
+
this._username = await this.syncStrategy.getCurrentUsername();
|
|
2018
|
+
await this.init();
|
|
2019
|
+
return ret;
|
|
2057
2020
|
}
|
|
2058
|
-
|
|
2059
|
-
return
|
|
2021
|
+
update(id, update) {
|
|
2022
|
+
return this.updateQueue.update(id, update);
|
|
2060
2023
|
}
|
|
2061
|
-
async
|
|
2024
|
+
async getCourseRegistrationsDoc() {
|
|
2025
|
+
logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
|
|
2026
|
+
let ret;
|
|
2062
2027
|
try {
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
`[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
|
|
2068
|
-
);
|
|
2069
|
-
return {
|
|
2070
|
-
status: Status2.error,
|
|
2071
|
-
message: `Note was added but no cards were created: ${resp.cardCreationError}`,
|
|
2072
|
-
id: resp.id
|
|
2073
|
-
};
|
|
2074
|
-
}
|
|
2075
|
-
return {
|
|
2076
|
-
status: Status2.ok,
|
|
2077
|
-
message: "",
|
|
2078
|
-
id: resp.id
|
|
2079
|
-
};
|
|
2080
|
-
} else {
|
|
2081
|
-
return {
|
|
2082
|
-
status: Status2.error,
|
|
2083
|
-
message: "Unexpected error adding note"
|
|
2084
|
-
};
|
|
2085
|
-
}
|
|
2028
|
+
const regDoc = await this.localDB.get(
|
|
2029
|
+
_BaseUser.DOC_IDS.COURSE_REGISTRATIONS
|
|
2030
|
+
);
|
|
2031
|
+
return regDoc;
|
|
2086
2032
|
} catch (e) {
|
|
2087
2033
|
const err = e;
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2034
|
+
if (err.status === 404) {
|
|
2035
|
+
await this.localDB.put({
|
|
2036
|
+
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
2037
|
+
courses: [],
|
|
2038
|
+
studyWeight: {}
|
|
2039
|
+
});
|
|
2040
|
+
ret = await this.getCourseRegistrationsDoc();
|
|
2041
|
+
} else {
|
|
2042
|
+
throw new Error(
|
|
2043
|
+
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2097
2046
|
}
|
|
2047
|
+
return ret;
|
|
2098
2048
|
}
|
|
2099
|
-
async
|
|
2100
|
-
|
|
2049
|
+
async getActiveCourses() {
|
|
2050
|
+
const reg = await this.getCourseRegistrationsDoc();
|
|
2051
|
+
return reg.courses.filter((c) => {
|
|
2052
|
+
return c.status === void 0 || c.status === "active";
|
|
2053
|
+
});
|
|
2101
2054
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2055
|
+
/**
|
|
2056
|
+
* Returns a promise of the card IDs that the user has
|
|
2057
|
+
* a scheduled review for.
|
|
2058
|
+
*
|
|
2059
|
+
*/
|
|
2060
|
+
async getActiveCards() {
|
|
2061
|
+
const keys = getStartAndEndKeys2(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
|
|
2062
|
+
const reviews = await this.remoteDB.allDocs({
|
|
2063
|
+
startkey: keys.startkey,
|
|
2064
|
+
endkey: keys.endkey,
|
|
2065
|
+
include_docs: true
|
|
2066
|
+
});
|
|
2067
|
+
return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
|
|
2104
2068
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2069
|
+
async getActivityRecords() {
|
|
2070
|
+
try {
|
|
2071
|
+
const hist = await this.getHistory();
|
|
2072
|
+
const allRecords = [];
|
|
2073
|
+
if (!Array.isArray(hist)) {
|
|
2074
|
+
logger.error("getHistory did not return an array:", hist);
|
|
2075
|
+
return allRecords;
|
|
2076
|
+
}
|
|
2077
|
+
let sampleCount = 0;
|
|
2078
|
+
for (let i = 0; i < hist.length; i++) {
|
|
2079
|
+
try {
|
|
2080
|
+
if (hist[i] && Array.isArray(hist[i].records)) {
|
|
2081
|
+
hist[i].records.forEach((record) => {
|
|
2082
|
+
try {
|
|
2083
|
+
if (!record.timeStamp) {
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
let timeStamp;
|
|
2087
|
+
if (typeof record.timeStamp === "object") {
|
|
2088
|
+
if (typeof record.timeStamp.toDate === "function") {
|
|
2089
|
+
timeStamp = record.timeStamp.toISOString();
|
|
2090
|
+
} else if (record.timeStamp instanceof Date) {
|
|
2091
|
+
timeStamp = record.timeStamp.toISOString();
|
|
2092
|
+
} else {
|
|
2093
|
+
if (sampleCount < 3) {
|
|
2094
|
+
logger.warn("Unknown timestamp object type:", record.timeStamp);
|
|
2095
|
+
sampleCount++;
|
|
2096
|
+
}
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
} else if (typeof record.timeStamp === "string") {
|
|
2100
|
+
const date = new Date(record.timeStamp);
|
|
2101
|
+
if (isNaN(date.getTime())) {
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
timeStamp = record.timeStamp;
|
|
2105
|
+
} else if (typeof record.timeStamp === "number") {
|
|
2106
|
+
timeStamp = new Date(record.timeStamp).toISOString();
|
|
2107
|
+
} else {
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
allRecords.push({
|
|
2111
|
+
timeStamp,
|
|
2112
|
+
courseID: record.courseID || "unknown",
|
|
2113
|
+
cardID: record.cardID || "unknown",
|
|
2114
|
+
timeSpent: record.timeSpent || 0,
|
|
2115
|
+
type: "card_view"
|
|
2116
|
+
});
|
|
2117
|
+
} catch (err) {
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
} catch (err) {
|
|
2122
|
+
logger.error("Error processing history item:", err);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
logger.debug(`Found ${allRecords.length} activity records`);
|
|
2126
|
+
return allRecords;
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
logger.error("Error in getActivityRecords:", err);
|
|
2129
|
+
return [];
|
|
2130
|
+
}
|
|
2121
2131
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2132
|
+
async getReviewstoDate(targetDate, course_id) {
|
|
2133
|
+
const keys = getStartAndEndKeys2(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
|
|
2134
|
+
const reviews = await this.remoteDB.allDocs({
|
|
2135
|
+
startkey: keys.startkey,
|
|
2136
|
+
endkey: keys.endkey,
|
|
2137
|
+
include_docs: true
|
|
2138
|
+
});
|
|
2139
|
+
log3(
|
|
2140
|
+
`Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
|
|
2141
|
+
);
|
|
2142
|
+
return reviews.rows.filter((r) => {
|
|
2143
|
+
if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
|
|
2144
|
+
const date = moment4.utc(
|
|
2145
|
+
r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
|
|
2146
|
+
REVIEW_TIME_FORMAT2
|
|
2147
|
+
);
|
|
2148
|
+
if (targetDate.isAfter(date)) {
|
|
2149
|
+
if (course_id === void 0 || r.doc.courseId === course_id) {
|
|
2150
|
+
return true;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2134
2153
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2154
|
+
}).map((r) => r.doc);
|
|
2155
|
+
}
|
|
2156
|
+
async getReviewsForcast(daysCount) {
|
|
2157
|
+
const time = moment4.utc().add(daysCount, "days");
|
|
2158
|
+
return this.getReviewstoDate(time);
|
|
2137
2159
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
return Promise.resolve();
|
|
2160
|
+
async getPendingReviews(course_id) {
|
|
2161
|
+
const now = moment4.utc();
|
|
2162
|
+
return this.getReviewstoDate(now, course_id);
|
|
2142
2163
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
logger.debug(JSON.stringify(data));
|
|
2146
|
-
return Promise.resolve();
|
|
2164
|
+
async getScheduledReviewCount(course_id) {
|
|
2165
|
+
return (await this.getPendingReviews(course_id)).length;
|
|
2147
2166
|
}
|
|
2148
|
-
async
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
name: "ELO",
|
|
2154
|
-
description: "ELO-based navigation strategy",
|
|
2155
|
-
implementingClass: "elo" /* ELO */,
|
|
2156
|
-
course: this.id,
|
|
2157
|
-
serializedData: ""
|
|
2158
|
-
// serde is a noop for ELO navigator.
|
|
2159
|
-
};
|
|
2160
|
-
return Promise.resolve(ret);
|
|
2167
|
+
async getRegisteredCourses() {
|
|
2168
|
+
const regDoc = await this.getCourseRegistrationsDoc();
|
|
2169
|
+
return regDoc.courses.filter((c) => {
|
|
2170
|
+
return !c.status || c.status === "active" || c.status === "maintenance-mode";
|
|
2171
|
+
});
|
|
2161
2172
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
const u = await this._getCurrentUser();
|
|
2170
|
-
try {
|
|
2171
|
-
const strategy = await this.surfaceNavigationStrategy();
|
|
2172
|
-
const navigator = await ContentNavigator.create(u, this, strategy);
|
|
2173
|
-
return navigator.getNewCards(limit);
|
|
2174
|
-
} catch (e) {
|
|
2175
|
-
logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
|
|
2176
|
-
throw e;
|
|
2173
|
+
async getCourseRegDoc(courseID) {
|
|
2174
|
+
const regDocs = await this.getCourseRegistrationsDoc();
|
|
2175
|
+
const ret = regDocs.courses.find((c) => c.courseID === courseID);
|
|
2176
|
+
if (ret) {
|
|
2177
|
+
return ret;
|
|
2178
|
+
} else {
|
|
2179
|
+
throw new Error(`Course registration not found for course ID: ${courseID}`);
|
|
2177
2180
|
}
|
|
2178
2181
|
}
|
|
2179
|
-
async
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
const
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2182
|
+
async registerForCourse(course_id, previewMode = false) {
|
|
2183
|
+
return this.getCourseRegistrationsDoc().then((doc) => {
|
|
2184
|
+
const status = previewMode ? "preview" : "active";
|
|
2185
|
+
logger.debug(`Registering for ${course_id} with status: ${status}`);
|
|
2186
|
+
const regItem = {
|
|
2187
|
+
status,
|
|
2188
|
+
courseID: course_id,
|
|
2189
|
+
user: true,
|
|
2190
|
+
admin: false,
|
|
2191
|
+
moderator: false,
|
|
2192
|
+
elo: {
|
|
2193
|
+
global: {
|
|
2194
|
+
score: 1e3,
|
|
2195
|
+
count: 0
|
|
2196
|
+
},
|
|
2197
|
+
tags: {},
|
|
2198
|
+
misc: {}
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
if (doc.courses.filter((course) => {
|
|
2202
|
+
return course.courseID === regItem.courseID;
|
|
2203
|
+
}).length === 0) {
|
|
2204
|
+
log3(`It's a new course registration!`);
|
|
2205
|
+
doc.courses.push(regItem);
|
|
2206
|
+
doc.studyWeight[course_id] = 1;
|
|
2207
|
+
} else {
|
|
2208
|
+
doc.courses.forEach((c) => {
|
|
2209
|
+
log3(`Found the previously registered course!`);
|
|
2210
|
+
if (c.courseID === course_id) {
|
|
2211
|
+
c.status = status;
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
return this.localDB.put(doc);
|
|
2216
|
+
}).catch((e) => {
|
|
2217
|
+
log3(`Registration failed because of: ${JSON.stringify(e)}`);
|
|
2187
2218
|
throw e;
|
|
2188
|
-
}
|
|
2219
|
+
});
|
|
2189
2220
|
}
|
|
2190
|
-
async
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
targetElo = -1;
|
|
2198
|
-
try {
|
|
2199
|
-
const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
|
|
2200
|
-
return c.courseID === this.id;
|
|
2201
|
-
});
|
|
2202
|
-
targetElo = EloToNumber(courseDoc.elo);
|
|
2203
|
-
} catch {
|
|
2204
|
-
targetElo = 1e3;
|
|
2221
|
+
async dropCourse(course_id, dropStatus = "dropped") {
|
|
2222
|
+
return this.getCourseRegistrationsDoc().then((doc) => {
|
|
2223
|
+
let index = -1;
|
|
2224
|
+
for (let i = 0; i < doc.courses.length; i++) {
|
|
2225
|
+
if (doc.courses[i].courseID === course_id) {
|
|
2226
|
+
index = i;
|
|
2227
|
+
}
|
|
2205
2228
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
let mult = 4;
|
|
2214
|
-
let previousCount = -1;
|
|
2215
|
-
let newCount = 0;
|
|
2216
|
-
while (cards.length < options.limit && newCount !== previousCount) {
|
|
2217
|
-
cards = await this.getCardsByELO(targetElo, mult * options.limit);
|
|
2218
|
-
previousCount = newCount;
|
|
2219
|
-
newCount = cards.length;
|
|
2220
|
-
logger.debug(`Found ${cards.length} elo neighbor cards...`);
|
|
2221
|
-
if (filter) {
|
|
2222
|
-
cards = cards.filter(filter);
|
|
2223
|
-
logger.debug(`Filtered to ${cards.length} cards...`);
|
|
2229
|
+
if (index !== -1) {
|
|
2230
|
+
delete doc.studyWeight[course_id];
|
|
2231
|
+
doc.courses[index].status = dropStatus;
|
|
2232
|
+
} else {
|
|
2233
|
+
throw new Error(
|
|
2234
|
+
`User ${this.getUsername()} is not currently registered for course ${course_id}`
|
|
2235
|
+
);
|
|
2224
2236
|
}
|
|
2225
|
-
|
|
2226
|
-
}
|
|
2227
|
-
const selectedCards = [];
|
|
2228
|
-
while (selectedCards.length < options.limit && cards.length > 0) {
|
|
2229
|
-
const index = randIntWeightedTowardZero(cards.length);
|
|
2230
|
-
const card = cards.splice(index, 1)[0];
|
|
2231
|
-
selectedCards.push(card);
|
|
2232
|
-
}
|
|
2233
|
-
return selectedCards.map((c) => {
|
|
2234
|
-
const split = c.split("-");
|
|
2235
|
-
return {
|
|
2236
|
-
courseID: this.id,
|
|
2237
|
-
cardID: split[1],
|
|
2238
|
-
qualifiedID: `${split[0]}-${split[1]}`,
|
|
2239
|
-
contentSourceType: "course",
|
|
2240
|
-
contentSourceID: this.id,
|
|
2241
|
-
status: "new"
|
|
2242
|
-
};
|
|
2237
|
+
return this.localDB.put(doc);
|
|
2243
2238
|
});
|
|
2244
2239
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
});
|
|
2248
|
-
|
|
2249
|
-
// src/impl/couch/classroomDB.ts
|
|
2250
|
-
import moment4 from "moment";
|
|
2251
|
-
function getClassroomDB(classID, version) {
|
|
2252
|
-
const dbName = `classdb-${version}-${classID}`;
|
|
2253
|
-
logger.info(`Retrieving classroom db: ${dbName}`);
|
|
2254
|
-
return new pouchdb_setup_default(
|
|
2255
|
-
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2256
|
-
pouchDBincludeCredentialsConfig
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
|
-
async function getClassroomConfig(classID) {
|
|
2260
|
-
return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
|
|
2261
|
-
}
|
|
2262
|
-
var classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
|
|
2263
|
-
var init_classroomDB = __esm({
|
|
2264
|
-
"src/impl/couch/classroomDB.ts"() {
|
|
2265
|
-
"use strict";
|
|
2266
|
-
init_factory();
|
|
2267
|
-
init_logger();
|
|
2268
|
-
init_pouchdb_setup();
|
|
2269
|
-
init_couch();
|
|
2270
|
-
init_courseDB();
|
|
2271
|
-
classroomLookupDBTitle = "classdb-lookup";
|
|
2272
|
-
CLASSROOM_CONFIG = "ClassroomConfig";
|
|
2273
|
-
ClassroomDBBase = class {
|
|
2274
|
-
_id;
|
|
2275
|
-
_db;
|
|
2276
|
-
_cfg;
|
|
2277
|
-
_initComplete = false;
|
|
2278
|
-
_content_prefix = "content";
|
|
2279
|
-
get _content_searchkeys() {
|
|
2280
|
-
return getStartAndEndKeys2(this._content_prefix);
|
|
2240
|
+
async getCourseInterface(courseId) {
|
|
2241
|
+
return new UsrCrsData(this, courseId);
|
|
2281
2242
|
}
|
|
2282
|
-
async
|
|
2283
|
-
|
|
2284
|
-
const
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2243
|
+
async getUserEditableCourses() {
|
|
2244
|
+
let courseIDs = [];
|
|
2245
|
+
const registeredCourses = await this.getCourseRegistrationsDoc();
|
|
2246
|
+
courseIDs = courseIDs.concat(
|
|
2247
|
+
registeredCourses.courses.map((course) => {
|
|
2248
|
+
return course.courseID;
|
|
2249
|
+
})
|
|
2250
|
+
);
|
|
2251
|
+
const cfgs = await Promise.all(
|
|
2252
|
+
courseIDs.map(async (id) => {
|
|
2253
|
+
return await getCredentialledCourseConfig(id);
|
|
2254
|
+
})
|
|
2255
|
+
);
|
|
2256
|
+
return cfgs;
|
|
2293
2257
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2258
|
+
async getConfig() {
|
|
2259
|
+
const defaultConfig = {
|
|
2260
|
+
_id: _BaseUser.DOC_IDS.CONFIG,
|
|
2261
|
+
darkMode: false,
|
|
2262
|
+
likesConfetti: false,
|
|
2263
|
+
sessionTimeLimit: 5
|
|
2264
|
+
};
|
|
2265
|
+
try {
|
|
2266
|
+
const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
|
|
2267
|
+
logger.debug("Raw config from DB:", cfg);
|
|
2268
|
+
return cfg;
|
|
2269
|
+
} catch (e) {
|
|
2270
|
+
const err = e;
|
|
2271
|
+
if (err.name && err.name === "not_found") {
|
|
2272
|
+
await this.localDB.put(defaultConfig);
|
|
2273
|
+
return this.getConfig();
|
|
2274
|
+
} else {
|
|
2275
|
+
logger.error(`Error setting user default config:`, e);
|
|
2276
|
+
throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
async setConfig(items) {
|
|
2281
|
+
logger.debug(`Setting Config items ${JSON.stringify(items)}`);
|
|
2282
|
+
const c = await this.getConfig();
|
|
2283
|
+
const put = await this.localDB.put({
|
|
2284
|
+
...c,
|
|
2285
|
+
...items
|
|
2286
|
+
});
|
|
2287
|
+
if (put.ok) {
|
|
2288
|
+
logger.debug(`Config items set: ${JSON.stringify(items)}`);
|
|
2297
2289
|
} else {
|
|
2298
|
-
|
|
2290
|
+
logger.error(`Error setting config items: ${JSON.stringify(put)}`);
|
|
2299
2291
|
}
|
|
2300
2292
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2293
|
+
/**
|
|
2294
|
+
*
|
|
2295
|
+
* This function should be called *only* by the pouchdb datalayer provider
|
|
2296
|
+
* auth store.
|
|
2297
|
+
*
|
|
2298
|
+
*
|
|
2299
|
+
* Anyone else seeking the current user should use the auth store's
|
|
2300
|
+
* exported `getCurrentUser` method.
|
|
2301
|
+
*
|
|
2302
|
+
*/
|
|
2303
|
+
static async instance(syncStrategy, username) {
|
|
2304
|
+
if (username) {
|
|
2305
|
+
_BaseUser._instance = new _BaseUser(username, syncStrategy);
|
|
2306
|
+
await _BaseUser._instance.init();
|
|
2307
|
+
return _BaseUser._instance;
|
|
2308
|
+
} else if (_BaseUser._instance && _BaseUser._initialized) {
|
|
2309
|
+
return _BaseUser._instance;
|
|
2310
|
+
} else if (_BaseUser._instance) {
|
|
2311
|
+
return new Promise((resolve) => {
|
|
2312
|
+
(function waitForUser() {
|
|
2313
|
+
if (_BaseUser._initialized) {
|
|
2314
|
+
return resolve(_BaseUser._instance);
|
|
2315
|
+
} else {
|
|
2316
|
+
setTimeout(waitForUser, 50);
|
|
2317
|
+
}
|
|
2318
|
+
})();
|
|
2319
|
+
});
|
|
2320
|
+
} else {
|
|
2321
|
+
const guestUsername = await syncStrategy.getCurrentUsername();
|
|
2322
|
+
_BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
|
|
2323
|
+
await _BaseUser._instance.init();
|
|
2324
|
+
return _BaseUser._instance;
|
|
2325
|
+
}
|
|
2303
2326
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2327
|
+
constructor(username, syncStrategy) {
|
|
2328
|
+
_BaseUser._initialized = false;
|
|
2329
|
+
this._username = username;
|
|
2330
|
+
this.syncStrategy = syncStrategy;
|
|
2331
|
+
this.setDBandQ();
|
|
2306
2332
|
}
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
constructor(classID, user) {
|
|
2313
|
-
super();
|
|
2314
|
-
this._id = classID;
|
|
2315
|
-
this._user = user;
|
|
2333
|
+
setDBandQ() {
|
|
2334
|
+
this.localDB = getLocalUserDB(this._username);
|
|
2335
|
+
this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
|
|
2336
|
+
this.writeDB = this.syncStrategy.getWriteDB ? this.syncStrategy.getWriteDB(this._username) : this.localDB;
|
|
2337
|
+
this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
|
|
2316
2338
|
}
|
|
2317
2339
|
async init() {
|
|
2318
|
-
|
|
2319
|
-
this.
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2340
|
+
_BaseUser._initialized = false;
|
|
2341
|
+
if (this._username === "admin") {
|
|
2342
|
+
_BaseUser._initialized = true;
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
this.setDBandQ();
|
|
2346
|
+
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
2347
|
+
void this.applyDesignDocs();
|
|
2348
|
+
void this.deduplicateReviews();
|
|
2349
|
+
_BaseUser._initialized = true;
|
|
2350
|
+
}
|
|
2351
|
+
static designDocs = [
|
|
2352
|
+
{
|
|
2353
|
+
_id: "_design/reviewCards",
|
|
2354
|
+
views: {
|
|
2355
|
+
reviewCards: {
|
|
2356
|
+
map: `function (doc) {
|
|
2357
|
+
if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
|
|
2358
|
+
emit(doc._id, doc.courseId + '-' + doc.cardId);
|
|
2359
|
+
}
|
|
2360
|
+
}`
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
];
|
|
2365
|
+
async applyDesignDocs() {
|
|
2366
|
+
if (this._username === "admin") {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
for (const doc of _BaseUser.designDocs) {
|
|
2370
|
+
try {
|
|
2371
|
+
try {
|
|
2372
|
+
const existingDoc = await this.remoteDB.get(doc._id);
|
|
2373
|
+
await this.remoteDB.put({
|
|
2374
|
+
...doc,
|
|
2375
|
+
_rev: existingDoc._rev
|
|
2376
|
+
});
|
|
2377
|
+
} catch (e) {
|
|
2378
|
+
if (e instanceof Error && e.name === "not_found") {
|
|
2379
|
+
await this.remoteDB.put(doc);
|
|
2380
|
+
} else {
|
|
2381
|
+
throw e;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
if (error.name && error.name === "conflict") {
|
|
2386
|
+
logger.warn(`Design doc ${doc._id} update conflict - will retry`);
|
|
2387
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2388
|
+
await this.applyDesignDoc(doc);
|
|
2389
|
+
} else {
|
|
2390
|
+
logger.error(`Failed to apply design doc ${doc._id}:`, error);
|
|
2391
|
+
throw error;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
// Helper method for single doc update with retry
|
|
2397
|
+
async applyDesignDoc(doc, retries = 3) {
|
|
2323
2398
|
try {
|
|
2324
|
-
const
|
|
2325
|
-
this.
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
live: true,
|
|
2329
|
-
include_docs: true
|
|
2399
|
+
const existingDoc = await this.remoteDB.get(doc._id);
|
|
2400
|
+
await this.remoteDB.put({
|
|
2401
|
+
...doc,
|
|
2402
|
+
_rev: existingDoc._rev
|
|
2330
2403
|
});
|
|
2331
|
-
this._initComplete = true;
|
|
2332
|
-
return;
|
|
2333
2404
|
} catch (e) {
|
|
2334
|
-
|
|
2405
|
+
if (e instanceof Error && e.name === "conflict" && retries > 0) {
|
|
2406
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2407
|
+
return this.applyDesignDoc(doc, retries - 1);
|
|
2408
|
+
}
|
|
2409
|
+
throw e;
|
|
2335
2410
|
}
|
|
2336
2411
|
}
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2412
|
+
/**
|
|
2413
|
+
* Logs a record of the user's interaction with the card and returns the card's
|
|
2414
|
+
* up-to-date history
|
|
2415
|
+
*
|
|
2416
|
+
* // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
|
|
2417
|
+
*
|
|
2418
|
+
* @param record the recent recorded interaction between user and card
|
|
2419
|
+
* @returns The updated state of the card's CardHistory data
|
|
2420
|
+
*/
|
|
2421
|
+
async putCardRecord(record) {
|
|
2422
|
+
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
2423
|
+
record.timeStamp = moment4.utc(record.timeStamp).toString();
|
|
2424
|
+
try {
|
|
2425
|
+
const cardHistory = await this.update(
|
|
2426
|
+
cardHistoryID,
|
|
2427
|
+
function(h) {
|
|
2428
|
+
h.records.push(record);
|
|
2429
|
+
h.bestInterval = h.bestInterval || 0;
|
|
2430
|
+
h.lapses = h.lapses || 0;
|
|
2431
|
+
h.streak = h.streak || 0;
|
|
2432
|
+
return h;
|
|
2433
|
+
}
|
|
2434
|
+
);
|
|
2435
|
+
cardHistory.records = cardHistory.records.map((record2) => {
|
|
2436
|
+
const ret = {
|
|
2437
|
+
...record2
|
|
2438
|
+
};
|
|
2439
|
+
ret.timeStamp = moment4.utc(record2.timeStamp);
|
|
2440
|
+
return ret;
|
|
2441
|
+
});
|
|
2442
|
+
return cardHistory;
|
|
2443
|
+
} catch (e) {
|
|
2444
|
+
const reason = e;
|
|
2445
|
+
if (reason.status === 404) {
|
|
2446
|
+
const initCardHistory = {
|
|
2447
|
+
_id: cardHistoryID,
|
|
2448
|
+
cardID: record.cardID,
|
|
2449
|
+
courseID: record.courseID,
|
|
2450
|
+
records: [record],
|
|
2451
|
+
lapses: 0,
|
|
2452
|
+
streak: 0,
|
|
2453
|
+
bestInterval: 0
|
|
2454
|
+
};
|
|
2455
|
+
const putResult = await this.writeDB.put(initCardHistory);
|
|
2456
|
+
return { ...initCardHistory, _rev: putResult.rev };
|
|
2457
|
+
} else {
|
|
2458
|
+
throw new Error(`putCardRecord failed because of:
|
|
2459
|
+
name:${reason.name}
|
|
2460
|
+
error: ${reason.error}
|
|
2461
|
+
message: ${reason.message}`);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2341
2464
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2465
|
+
async deduplicateReviews() {
|
|
2466
|
+
try {
|
|
2467
|
+
log3("Starting deduplication of scheduled reviews...");
|
|
2468
|
+
const reviewsMap = {};
|
|
2469
|
+
const duplicateDocIds = [];
|
|
2470
|
+
const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
|
|
2471
|
+
log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
|
|
2472
|
+
scheduledReviews.rows.forEach((r) => {
|
|
2473
|
+
const qualifiedCardId = r.value;
|
|
2474
|
+
const docId = r.key;
|
|
2475
|
+
if (reviewsMap[qualifiedCardId]) {
|
|
2476
|
+
log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
|
|
2477
|
+
log3(
|
|
2478
|
+
`Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
|
|
2479
|
+
);
|
|
2480
|
+
duplicateDocIds.push(reviewsMap[qualifiedCardId]);
|
|
2481
|
+
reviewsMap[qualifiedCardId] = docId;
|
|
2482
|
+
} else {
|
|
2483
|
+
reviewsMap[qualifiedCardId] = docId;
|
|
2484
|
+
}
|
|
2485
|
+
});
|
|
2486
|
+
if (duplicateDocIds.length > 0) {
|
|
2487
|
+
log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
|
|
2488
|
+
const deletePromises = duplicateDocIds.map(async (docId) => {
|
|
2489
|
+
try {
|
|
2490
|
+
const doc = await this.remoteDB.get(docId);
|
|
2491
|
+
await this.writeDB.remove(doc);
|
|
2492
|
+
log3(`Successfully removed duplicate review: ${docId}`);
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
log3(`Failed to remove duplicate review ${docId}: ${error}`);
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
await Promise.all(deletePromises);
|
|
2498
|
+
log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
|
|
2499
|
+
} else {
|
|
2500
|
+
log3("No duplicate reviews found");
|
|
2501
|
+
}
|
|
2502
|
+
} catch (error) {
|
|
2503
|
+
log3(`Error during review deduplication: ${error}`);
|
|
2504
|
+
}
|
|
2344
2505
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2506
|
+
/**
|
|
2507
|
+
* Returns a promise of the card IDs that the user has
|
|
2508
|
+
* encountered in the past.
|
|
2509
|
+
*
|
|
2510
|
+
* @param course_id optional specification of individual course
|
|
2511
|
+
*/
|
|
2512
|
+
async getSeenCards(course_id) {
|
|
2513
|
+
let prefix = DocTypePrefixes["CARDRECORD" /* CARDRECORD */];
|
|
2514
|
+
if (course_id) {
|
|
2515
|
+
prefix += course_id;
|
|
2516
|
+
}
|
|
2517
|
+
const docs = await filterAllDocsByPrefix2(this.localDB, prefix, {
|
|
2518
|
+
include_docs: false
|
|
2519
|
+
});
|
|
2520
|
+
const ret = [];
|
|
2521
|
+
docs.rows.forEach((row) => {
|
|
2522
|
+
if (row.id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */])) {
|
|
2523
|
+
ret.push(row.id.substr(DocTypePrefixes["CARDRECORD" /* CARDRECORD */].length));
|
|
2524
|
+
}
|
|
2358
2525
|
});
|
|
2526
|
+
return ret;
|
|
2359
2527
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
ret = ret.concat(await db.getNewCards());
|
|
2372
|
-
} else if (content.type === "tag") {
|
|
2373
|
-
const tagDoc = await getTag(content.courseID, content.tagID);
|
|
2374
|
-
ret = ret.concat(
|
|
2375
|
-
tagDoc.taggedCards.map((c) => {
|
|
2376
|
-
return {
|
|
2377
|
-
courseID: content.courseID,
|
|
2378
|
-
cardID: c,
|
|
2379
|
-
qualifiedID: `${content.courseID}-${c}`,
|
|
2380
|
-
contentSourceType: "classroom",
|
|
2381
|
-
contentSourceID: this._id,
|
|
2382
|
-
status: "new"
|
|
2383
|
-
};
|
|
2384
|
-
})
|
|
2385
|
-
);
|
|
2386
|
-
} else if (content.type === "card") {
|
|
2387
|
-
ret.push(await getCourseDB2(content.courseID).get(content.cardID));
|
|
2528
|
+
/**
|
|
2529
|
+
*
|
|
2530
|
+
* @returns A promise of the cards that the user has seen in the past.
|
|
2531
|
+
*/
|
|
2532
|
+
async getHistory() {
|
|
2533
|
+
const cards = await filterAllDocsByPrefix2(
|
|
2534
|
+
this.remoteDB,
|
|
2535
|
+
DocTypePrefixes["CARDRECORD" /* CARDRECORD */],
|
|
2536
|
+
{
|
|
2537
|
+
include_docs: true,
|
|
2538
|
+
attachments: false
|
|
2388
2539
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2540
|
+
);
|
|
2541
|
+
return cards.rows.map((r) => r.doc);
|
|
2542
|
+
}
|
|
2543
|
+
async updateCourseSettings(course_id, settings) {
|
|
2544
|
+
void this.getCourseRegistrationsDoc().then((doc) => {
|
|
2545
|
+
const crs = doc.courses.find((c) => c.courseID === course_id);
|
|
2546
|
+
if (crs) {
|
|
2547
|
+
if (crs.settings === null || crs.settings === void 0) {
|
|
2548
|
+
crs.settings = {};
|
|
2549
|
+
}
|
|
2550
|
+
settings.forEach((setting) => {
|
|
2551
|
+
crs.settings[setting.key] = setting.value;
|
|
2552
|
+
});
|
|
2396
2553
|
}
|
|
2554
|
+
return this.localDB.put(doc);
|
|
2397
2555
|
});
|
|
2398
2556
|
}
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2557
|
+
async getCourseSettings(course_id) {
|
|
2558
|
+
const regDoc = await this.getCourseRegistrationsDoc();
|
|
2559
|
+
const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
|
|
2560
|
+
if (crsDoc) {
|
|
2561
|
+
return crsDoc.settings;
|
|
2562
|
+
} else {
|
|
2563
|
+
throw new Error(`getCourseSettings Failed:
|
|
2564
|
+
User is not registered for course ${course_id}`);
|
|
2565
|
+
}
|
|
2405
2566
|
}
|
|
2406
|
-
async
|
|
2407
|
-
|
|
2408
|
-
const stuDbName = `classdb-student-${this._id}`;
|
|
2409
|
-
this._db = new pouchdb_setup_default(
|
|
2410
|
-
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2411
|
-
pouchDBincludeCredentialsConfig
|
|
2412
|
-
);
|
|
2413
|
-
this._stuDb = new pouchdb_setup_default(
|
|
2414
|
-
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
|
|
2415
|
-
pouchDBincludeCredentialsConfig
|
|
2416
|
-
);
|
|
2567
|
+
async getOrCreateClassroomRegistrationsDoc() {
|
|
2568
|
+
let ret;
|
|
2417
2569
|
try {
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
}).then(() => {
|
|
2422
|
-
return;
|
|
2423
|
-
});
|
|
2570
|
+
ret = await this.remoteDB.get(
|
|
2571
|
+
_BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
|
|
2572
|
+
);
|
|
2424
2573
|
} catch (e) {
|
|
2425
|
-
|
|
2574
|
+
const err = e;
|
|
2575
|
+
if (err.status === 404) {
|
|
2576
|
+
await this.writeDB.put({
|
|
2577
|
+
_id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
|
|
2578
|
+
registrations: []
|
|
2579
|
+
});
|
|
2580
|
+
ret = await this.getOrCreateClassroomRegistrationsDoc();
|
|
2581
|
+
} else {
|
|
2582
|
+
const errorDetails = {
|
|
2583
|
+
name: err.name,
|
|
2584
|
+
status: err.status,
|
|
2585
|
+
message: err.message,
|
|
2586
|
+
reason: err.reason,
|
|
2587
|
+
error: err.error
|
|
2588
|
+
};
|
|
2589
|
+
logger.error(
|
|
2590
|
+
"Database error in getOrCreateClassroomRegistrationsDoc (private method):",
|
|
2591
|
+
errorDetails
|
|
2592
|
+
);
|
|
2593
|
+
throw new Error(
|
|
2594
|
+
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2426
2597
|
}
|
|
2427
|
-
|
|
2428
|
-
static async factory(classID) {
|
|
2429
|
-
const ret = new _TeacherClassroomDB(classID);
|
|
2430
|
-
await ret.init();
|
|
2598
|
+
logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
|
|
2431
2599
|
return ret;
|
|
2432
2600
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2601
|
+
/**
|
|
2602
|
+
* Retrieves the list of active classroom IDs where the user is registered as a student.
|
|
2603
|
+
*
|
|
2604
|
+
* @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
|
|
2605
|
+
* registration document is unavailable due to database errors
|
|
2606
|
+
*
|
|
2607
|
+
* @description This method gracefully handles database connectivity issues by returning
|
|
2608
|
+
* an empty array when the classroom registrations document cannot be accessed.
|
|
2609
|
+
* This ensures that users can still access other application features even
|
|
2610
|
+
* when classroom functionality is temporarily unavailable.
|
|
2611
|
+
*/
|
|
2612
|
+
async getActiveClasses() {
|
|
2435
2613
|
try {
|
|
2436
|
-
|
|
2437
|
-
await this._db.remove(doc);
|
|
2438
|
-
void this._db.replicate.to(this._stuDb, {
|
|
2439
|
-
doc_ids: [contentID]
|
|
2440
|
-
});
|
|
2614
|
+
return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
|
|
2441
2615
|
} catch (error) {
|
|
2442
|
-
logger.
|
|
2616
|
+
logger.warn(
|
|
2617
|
+
"Failed to load classroom registrations, continuing without classroom data:",
|
|
2618
|
+
error
|
|
2619
|
+
);
|
|
2620
|
+
return [];
|
|
2443
2621
|
}
|
|
2444
2622
|
}
|
|
2445
|
-
async
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
_id: id,
|
|
2463
|
-
assignedBy: content.assignedBy,
|
|
2464
|
-
assignedOn: moment4.utc(),
|
|
2465
|
-
activeOn: content.activeOn || moment4.utc()
|
|
2466
|
-
});
|
|
2467
|
-
}
|
|
2468
|
-
if (put.ok) {
|
|
2469
|
-
void this._db.replicate.to(this._stuDb, {
|
|
2470
|
-
doc_ids: [id]
|
|
2471
|
-
});
|
|
2472
|
-
return true;
|
|
2473
|
-
} else {
|
|
2474
|
-
return false;
|
|
2475
|
-
}
|
|
2623
|
+
async scheduleCardReview(review) {
|
|
2624
|
+
return scheduleCardReviewLocal(this.writeDB, review);
|
|
2625
|
+
}
|
|
2626
|
+
async removeScheduledCardReview(reviewId) {
|
|
2627
|
+
return removeScheduledCardReviewLocal(this.writeDB, reviewId);
|
|
2628
|
+
}
|
|
2629
|
+
async registerForClassroom(_classId, _registerAs) {
|
|
2630
|
+
return registerUserForClassroom(this._username, _classId, _registerAs);
|
|
2631
|
+
}
|
|
2632
|
+
async dropFromClassroom(classId) {
|
|
2633
|
+
return dropUserFromClassroom(this._username, classId);
|
|
2634
|
+
}
|
|
2635
|
+
async getUserClassrooms() {
|
|
2636
|
+
return getUserClassrooms(this._username);
|
|
2637
|
+
}
|
|
2638
|
+
async updateUserElo(courseId, elo) {
|
|
2639
|
+
return updateUserElo(this._username, courseId, elo);
|
|
2476
2640
|
}
|
|
2477
2641
|
};
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
});
|
|
2642
|
+
userCoursesDoc = "CourseRegistrations";
|
|
2643
|
+
userClassroomsDoc = "ClassroomRegistrations";
|
|
2481
2644
|
}
|
|
2482
2645
|
});
|
|
2483
2646
|
|
|
2484
|
-
// src/
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
return await StudentClassroomDB.factory(source.id, user);
|
|
2492
|
-
} else {
|
|
2493
|
-
return getDataLayer().getCourseDB(source.id);
|
|
2647
|
+
// src/impl/common/index.ts
|
|
2648
|
+
var init_common = __esm({
|
|
2649
|
+
"src/impl/common/index.ts"() {
|
|
2650
|
+
"use strict";
|
|
2651
|
+
init_SyncStrategy();
|
|
2652
|
+
init_BaseUserDB();
|
|
2653
|
+
init_userDBHelpers();
|
|
2494
2654
|
}
|
|
2655
|
+
});
|
|
2656
|
+
|
|
2657
|
+
// src/factory.ts
|
|
2658
|
+
function getDataLayer() {
|
|
2659
|
+
if (!dataLayerInstance) {
|
|
2660
|
+
throw new Error("Data layer not initialized. Call initializeDataLayer first.");
|
|
2661
|
+
}
|
|
2662
|
+
return dataLayerInstance;
|
|
2495
2663
|
}
|
|
2496
|
-
var
|
|
2497
|
-
|
|
2664
|
+
var ENV, dataLayerInstance;
|
|
2665
|
+
var init_factory = __esm({
|
|
2666
|
+
"src/factory.ts"() {
|
|
2498
2667
|
"use strict";
|
|
2499
|
-
|
|
2500
|
-
|
|
2668
|
+
init_common();
|
|
2669
|
+
init_logger();
|
|
2670
|
+
ENV = {
|
|
2671
|
+
COUCHDB_SERVER_PROTOCOL: "NOT_SET",
|
|
2672
|
+
COUCHDB_SERVER_URL: "NOT_SET"
|
|
2673
|
+
};
|
|
2674
|
+
dataLayerInstance = null;
|
|
2501
2675
|
}
|
|
2502
2676
|
});
|
|
2503
2677
|
|
|
2504
2678
|
// src/impl/couch/adminDB.ts
|
|
2505
2679
|
var AdminDB;
|
|
2506
|
-
var
|
|
2680
|
+
var init_adminDB2 = __esm({
|
|
2507
2681
|
"src/impl/couch/adminDB.ts"() {
|
|
2508
2682
|
"use strict";
|
|
2509
2683
|
init_pouchdb_setup();
|
|
2510
2684
|
init_factory();
|
|
2511
2685
|
init_couch();
|
|
2512
|
-
|
|
2686
|
+
init_classroomDB2();
|
|
2513
2687
|
init_courseLookupDB();
|
|
2514
2688
|
init_logger();
|
|
2515
2689
|
AdminDB = class {
|
|
@@ -2523,7 +2697,7 @@ var init_adminDB = __esm({
|
|
|
2523
2697
|
async getUsers() {
|
|
2524
2698
|
return (await this.usersDB.allDocs({
|
|
2525
2699
|
include_docs: true,
|
|
2526
|
-
...
|
|
2700
|
+
...getStartAndEndKeys("org.couchdb.user:")
|
|
2527
2701
|
})).rows.map((r) => r.doc);
|
|
2528
2702
|
}
|
|
2529
2703
|
async getCourses() {
|
|
@@ -2616,7 +2790,7 @@ var init_auth = __esm({
|
|
|
2616
2790
|
});
|
|
2617
2791
|
|
|
2618
2792
|
// src/impl/couch/CouchDBSyncStrategy.ts
|
|
2619
|
-
import { Status as
|
|
2793
|
+
import { Status as Status4 } from "@vue-skuilder/common";
|
|
2620
2794
|
var log4, CouchDBSyncStrategy;
|
|
2621
2795
|
var init_CouchDBSyncStrategy = __esm({
|
|
2622
2796
|
"src/impl/couch/CouchDBSyncStrategy.ts"() {
|
|
@@ -2641,6 +2815,13 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2641
2815
|
return this.getUserDB(username);
|
|
2642
2816
|
}
|
|
2643
2817
|
}
|
|
2818
|
+
getWriteDB(username) {
|
|
2819
|
+
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
2820
|
+
return getLocalUserDB(username);
|
|
2821
|
+
} else {
|
|
2822
|
+
return this.getUserDB(username);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2644
2825
|
startSync(localDB, remoteDB) {
|
|
2645
2826
|
if (localDB !== remoteDB) {
|
|
2646
2827
|
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
@@ -2675,32 +2856,32 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2675
2856
|
log4(`CREATEACCOUNT: logged in as new user: ${loginResult.ok}`);
|
|
2676
2857
|
if (loginResult.ok) {
|
|
2677
2858
|
return {
|
|
2678
|
-
status:
|
|
2859
|
+
status: Status4.ok,
|
|
2679
2860
|
error: void 0
|
|
2680
2861
|
};
|
|
2681
2862
|
} else {
|
|
2682
2863
|
return {
|
|
2683
|
-
status:
|
|
2864
|
+
status: Status4.error,
|
|
2684
2865
|
error: "Failed to log in after account creation"
|
|
2685
2866
|
};
|
|
2686
2867
|
}
|
|
2687
2868
|
} else {
|
|
2688
2869
|
logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
|
|
2689
2870
|
return {
|
|
2690
|
-
status:
|
|
2871
|
+
status: Status4.error,
|
|
2691
2872
|
error: "Account creation failed"
|
|
2692
2873
|
};
|
|
2693
2874
|
}
|
|
2694
2875
|
} catch (e) {
|
|
2695
2876
|
if (e.reason === "Document update conflict.") {
|
|
2696
2877
|
return {
|
|
2697
|
-
status:
|
|
2878
|
+
status: Status4.error,
|
|
2698
2879
|
error: "This username is taken!"
|
|
2699
2880
|
};
|
|
2700
2881
|
}
|
|
2701
2882
|
logger.error(`Error on signup: ${JSON.stringify(e)}`);
|
|
2702
2883
|
return {
|
|
2703
|
-
status:
|
|
2884
|
+
status: Status4.error,
|
|
2704
2885
|
error: e.message || "Unknown error during account creation"
|
|
2705
2886
|
};
|
|
2706
2887
|
}
|
|
@@ -2899,16 +3080,16 @@ function scheduleCardReview(review) {
|
|
|
2899
3080
|
const now = moment5.utc();
|
|
2900
3081
|
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
2901
3082
|
void getCouchUserDB(review.user).put({
|
|
2902
|
-
_id:
|
|
3083
|
+
_id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
|
|
2903
3084
|
cardId: review.card_id,
|
|
2904
|
-
reviewTime: review.time,
|
|
3085
|
+
reviewTime: review.time.toISOString(),
|
|
2905
3086
|
courseId: review.course_id,
|
|
2906
|
-
scheduledAt: now,
|
|
3087
|
+
scheduledAt: now.toISOString(),
|
|
2907
3088
|
scheduledFor: review.scheduledFor,
|
|
2908
3089
|
schedulingAgentId: review.schedulingAgentId
|
|
2909
3090
|
});
|
|
2910
3091
|
}
|
|
2911
|
-
function
|
|
3092
|
+
function filterAllDocsByPrefix(db, prefix, opts) {
|
|
2912
3093
|
const options = {
|
|
2913
3094
|
startkey: prefix,
|
|
2914
3095
|
endkey: prefix + "\uFFF0",
|
|
@@ -2919,13 +3100,13 @@ function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
|
2919
3100
|
}
|
|
2920
3101
|
return db.allDocs(options);
|
|
2921
3102
|
}
|
|
2922
|
-
function
|
|
3103
|
+
function getStartAndEndKeys(key) {
|
|
2923
3104
|
return {
|
|
2924
3105
|
startkey: key,
|
|
2925
3106
|
endkey: key + "\uFFF0"
|
|
2926
3107
|
};
|
|
2927
3108
|
}
|
|
2928
|
-
var isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig,
|
|
3109
|
+
var isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_TIME_FORMAT;
|
|
2929
3110
|
var init_couch = __esm({
|
|
2930
3111
|
"src/impl/couch/index.ts"() {
|
|
2931
3112
|
init_factory();
|
|
@@ -2933,8 +3114,8 @@ var init_couch = __esm({
|
|
|
2933
3114
|
init_logger();
|
|
2934
3115
|
init_pouchdb_setup();
|
|
2935
3116
|
init_contentSource();
|
|
2936
|
-
|
|
2937
|
-
|
|
3117
|
+
init_adminDB2();
|
|
3118
|
+
init_classroomDB2();
|
|
2938
3119
|
init_courseAPI();
|
|
2939
3120
|
init_courseDB();
|
|
2940
3121
|
init_CouchDBSyncStrategy();
|
|
@@ -2951,8 +3132,7 @@ var init_couch = __esm({
|
|
|
2951
3132
|
return pouchdb_setup_default.fetch(url, opts);
|
|
2952
3133
|
}
|
|
2953
3134
|
};
|
|
2954
|
-
|
|
2955
|
-
REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
3135
|
+
REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
2956
3136
|
}
|
|
2957
3137
|
});
|
|
2958
3138
|
init_couch();
|
|
@@ -2963,15 +3143,14 @@ export {
|
|
|
2963
3143
|
CouchDBSyncStrategy,
|
|
2964
3144
|
CourseDB,
|
|
2965
3145
|
CoursesDB,
|
|
2966
|
-
|
|
2967
|
-
REVIEW_TIME_FORMAT2 as REVIEW_TIME_FORMAT,
|
|
3146
|
+
REVIEW_TIME_FORMAT,
|
|
2968
3147
|
StudentClassroomDB,
|
|
2969
3148
|
TeacherClassroomDB,
|
|
2970
3149
|
addNote55,
|
|
2971
3150
|
addTagToCard,
|
|
2972
3151
|
createTag,
|
|
2973
3152
|
deleteTag,
|
|
2974
|
-
|
|
3153
|
+
filterAllDocsByPrefix,
|
|
2975
3154
|
getAncestorTagIDs,
|
|
2976
3155
|
getAppliedTags,
|
|
2977
3156
|
getChildTagStubs,
|
|
@@ -2988,7 +3167,7 @@ export {
|
|
|
2988
3167
|
getCredentialledDataShapes,
|
|
2989
3168
|
getLatestVersion,
|
|
2990
3169
|
getRandomCards,
|
|
2991
|
-
|
|
3170
|
+
getStartAndEndKeys,
|
|
2992
3171
|
getStudySource,
|
|
2993
3172
|
getTag,
|
|
2994
3173
|
getTagID,
|