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