@vue-skuilder/db 0.1.7 → 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 +131 -118
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +128 -115
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BbW9EnZK.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
- package/dist/{dataLayerProvider-6stCgDME.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 +1365 -1252
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +1359 -1246
- 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 +253 -843
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +250 -842
- 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 +10 -55
- package/dist/index.d.ts +10 -55
- package/dist/index.js +343 -170
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +340 -167
- package/dist/index.mjs.map +1 -1
- package/dist/{types-BvzcRAys.d.ts → types-BefDGkKa.d.ts} +1 -1
- package/dist/{types-CQQ80R5N.d.mts → types-DC-ckZug.d.mts} +1 -1
- package/dist/{types-legacy-CtrmkOLu.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
- package/dist/{types-legacy-CtrmkOLu.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
- package/dist/{userDB-DUY63VMN.d.ts → userDB-C33Hzjgn.d.mts} +10 -3
- package/dist/{userDB-7fM4tpgr.d.mts → userDB-DusL7OXe.d.ts} +10 -3
- 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/impl/common/BaseUserDB.ts +33 -22
- package/src/impl/common/SyncStrategy.ts +7 -0
- package/src/impl/common/index.ts +0 -1
- package/src/impl/common/userDBHelpers.ts +4 -4
- package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
- package/src/impl/couch/courseAPI.ts +7 -6
- 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/migrator/FileSystemAdapter.ts +20 -0
- package/src/util/migrator/StaticToCouchDBMigrator.ts +6 -0
- package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
package/dist/impl/couch/index.js
CHANGED
|
@@ -42,6 +42,20 @@ var init_SyncStrategy = __esm({
|
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
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
|
+
|
|
45
59
|
// src/util/logger.ts
|
|
46
60
|
var isDevelopment, logger;
|
|
47
61
|
var init_logger = __esm({
|
|
@@ -87,31 +101,6 @@ var init_logger = __esm({
|
|
|
87
101
|
}
|
|
88
102
|
});
|
|
89
103
|
|
|
90
|
-
// src/core/types/types-legacy.ts
|
|
91
|
-
var GuestUsername, log, cardHistoryPrefix;
|
|
92
|
-
var init_types_legacy = __esm({
|
|
93
|
-
"src/core/types/types-legacy.ts"() {
|
|
94
|
-
"use strict";
|
|
95
|
-
init_logger();
|
|
96
|
-
GuestUsername = "Guest";
|
|
97
|
-
log = (message) => {
|
|
98
|
-
logger.log(message);
|
|
99
|
-
};
|
|
100
|
-
cardHistoryPrefix = "cardH";
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// src/core/util/index.ts
|
|
105
|
-
function getCardHistoryID(courseID, cardID) {
|
|
106
|
-
return `${cardHistoryPrefix}-${courseID}-${cardID}`;
|
|
107
|
-
}
|
|
108
|
-
var init_util = __esm({
|
|
109
|
-
"src/core/util/index.ts"() {
|
|
110
|
-
"use strict";
|
|
111
|
-
init_types_legacy();
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
104
|
// src/impl/couch/pouchdb-setup.ts
|
|
116
105
|
var import_pouchdb, import_pouchdb_find, import_pouchdb_authentication, pouchdb_setup_default;
|
|
117
106
|
var init_pouchdb_setup = __esm({
|
|
@@ -131,123 +120,6 @@ var init_pouchdb_setup = __esm({
|
|
|
131
120
|
}
|
|
132
121
|
});
|
|
133
122
|
|
|
134
|
-
// src/util/tuiLogger.ts
|
|
135
|
-
var init_tuiLogger = __esm({
|
|
136
|
-
"src/util/tuiLogger.ts"() {
|
|
137
|
-
"use strict";
|
|
138
|
-
init_dataDirectory();
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// src/util/dataDirectory.ts
|
|
143
|
-
function getAppDataDirectory() {
|
|
144
|
-
return path.join(os.homedir(), ".tuilder");
|
|
145
|
-
}
|
|
146
|
-
function getDbPath(dbName) {
|
|
147
|
-
return path.join(getAppDataDirectory(), dbName);
|
|
148
|
-
}
|
|
149
|
-
var path, os;
|
|
150
|
-
var init_dataDirectory = __esm({
|
|
151
|
-
"src/util/dataDirectory.ts"() {
|
|
152
|
-
"use strict";
|
|
153
|
-
path = __toESM(require("path"));
|
|
154
|
-
os = __toESM(require("os"));
|
|
155
|
-
init_tuiLogger();
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// src/impl/common/userDBHelpers.ts
|
|
160
|
-
function hexEncode(str) {
|
|
161
|
-
let hex;
|
|
162
|
-
let returnStr = "";
|
|
163
|
-
for (let i = 0; i < str.length; i++) {
|
|
164
|
-
hex = str.charCodeAt(i).toString(16);
|
|
165
|
-
returnStr += ("000" + hex).slice(3);
|
|
166
|
-
}
|
|
167
|
-
return returnStr;
|
|
168
|
-
}
|
|
169
|
-
function filterAllDocsByPrefix(db, prefix, opts) {
|
|
170
|
-
const options = {
|
|
171
|
-
startkey: prefix,
|
|
172
|
-
endkey: prefix + "\uFFF0",
|
|
173
|
-
include_docs: true
|
|
174
|
-
};
|
|
175
|
-
if (opts) {
|
|
176
|
-
Object.assign(options, opts);
|
|
177
|
-
}
|
|
178
|
-
return db.allDocs(options);
|
|
179
|
-
}
|
|
180
|
-
function getStartAndEndKeys(key) {
|
|
181
|
-
return {
|
|
182
|
-
startkey: key,
|
|
183
|
-
endkey: key + "\uFFF0"
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
function updateGuestAccountExpirationDate(guestDB) {
|
|
187
|
-
const currentTime = import_moment.default.utc();
|
|
188
|
-
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
189
|
-
const expiryDocID2 = "GuestAccountExpirationDate";
|
|
190
|
-
void guestDB.get(expiryDocID2).then((doc) => {
|
|
191
|
-
return guestDB.put({
|
|
192
|
-
_id: expiryDocID2,
|
|
193
|
-
_rev: doc._rev,
|
|
194
|
-
date: expirationDate
|
|
195
|
-
});
|
|
196
|
-
}).catch(() => {
|
|
197
|
-
return guestDB.put({
|
|
198
|
-
_id: expiryDocID2,
|
|
199
|
-
date: expirationDate
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
function getLocalUserDB(username) {
|
|
204
|
-
const dbName = `userdb-${username}`;
|
|
205
|
-
if (typeof window === "undefined") {
|
|
206
|
-
return new pouchdb_setup_default(getDbPath(dbName), {});
|
|
207
|
-
} else {
|
|
208
|
-
return new pouchdb_setup_default(dbName, {});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
function scheduleCardReviewLocal(userDB, review) {
|
|
212
|
-
const now = import_moment.default.utc();
|
|
213
|
-
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
214
|
-
void userDB.put({
|
|
215
|
-
_id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
|
|
216
|
-
cardId: review.card_id,
|
|
217
|
-
reviewTime: review.time,
|
|
218
|
-
courseId: review.course_id,
|
|
219
|
-
scheduledAt: now,
|
|
220
|
-
scheduledFor: review.scheduledFor,
|
|
221
|
-
schedulingAgentId: review.schedulingAgentId
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
|
|
225
|
-
const reviewDoc = await userDB.get(reviewDocID);
|
|
226
|
-
userDB.remove(reviewDoc).then((res) => {
|
|
227
|
-
if (res.ok) {
|
|
228
|
-
log2(`Removed Review Doc: ${reviewDocID}`);
|
|
229
|
-
}
|
|
230
|
-
}).catch((err) => {
|
|
231
|
-
log2(`Failed to remove Review Doc: ${reviewDocID},
|
|
232
|
-
${JSON.stringify(err)}`);
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
var import_moment, REVIEW_PREFIX, REVIEW_TIME_FORMAT, log2;
|
|
236
|
-
var init_userDBHelpers = __esm({
|
|
237
|
-
"src/impl/common/userDBHelpers.ts"() {
|
|
238
|
-
"use strict";
|
|
239
|
-
import_moment = __toESM(require("moment"));
|
|
240
|
-
init_logger();
|
|
241
|
-
init_pouchdb_setup();
|
|
242
|
-
init_dataDirectory();
|
|
243
|
-
REVIEW_PREFIX = "card_review_";
|
|
244
|
-
REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
245
|
-
log2 = (s) => {
|
|
246
|
-
logger.info(s);
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
123
|
// src/util/Loggable.ts
|
|
252
124
|
var Loggable;
|
|
253
125
|
var init_Loggable = __esm({
|
|
@@ -275,7 +147,10 @@ var init_updateQueue = __esm({
|
|
|
275
147
|
_className = "UpdateQueue";
|
|
276
148
|
pendingUpdates = {};
|
|
277
149
|
inprogressUpdates = {};
|
|
278
|
-
|
|
150
|
+
readDB;
|
|
151
|
+
// Database for read operations
|
|
152
|
+
writeDB;
|
|
153
|
+
// Database for write operations (local-first)
|
|
279
154
|
update(id, update) {
|
|
280
155
|
logger.debug(`Update requested on doc: ${id}`);
|
|
281
156
|
if (this.pendingUpdates[id]) {
|
|
@@ -285,24 +160,25 @@ var init_updateQueue = __esm({
|
|
|
285
160
|
}
|
|
286
161
|
return this.applyUpdates(id);
|
|
287
162
|
}
|
|
288
|
-
constructor(
|
|
163
|
+
constructor(readDB, writeDB) {
|
|
289
164
|
super();
|
|
290
|
-
this.
|
|
165
|
+
this.readDB = readDB;
|
|
166
|
+
this.writeDB = writeDB || readDB;
|
|
291
167
|
logger.debug(`UpdateQ initialized...`);
|
|
292
|
-
void this.
|
|
168
|
+
void this.readDB.info().then((i) => {
|
|
293
169
|
logger.debug(`db info: ${JSON.stringify(i)}`);
|
|
294
170
|
});
|
|
295
171
|
}
|
|
296
172
|
async applyUpdates(id) {
|
|
297
173
|
logger.debug(`Applying updates on doc: ${id}`);
|
|
298
174
|
if (this.inprogressUpdates[id]) {
|
|
299
|
-
await this.
|
|
175
|
+
await this.readDB.info();
|
|
300
176
|
return this.applyUpdates(id);
|
|
301
177
|
} else {
|
|
302
178
|
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
303
179
|
this.inprogressUpdates[id] = true;
|
|
304
180
|
try {
|
|
305
|
-
let doc = await this.
|
|
181
|
+
let doc = await this.readDB.get(id);
|
|
306
182
|
logger.debug(`Retrieved doc: ${id}`);
|
|
307
183
|
while (this.pendingUpdates[id].length !== 0) {
|
|
308
184
|
const update = this.pendingUpdates[id].splice(0, 1)[0];
|
|
@@ -315,7 +191,7 @@ var init_updateQueue = __esm({
|
|
|
315
191
|
};
|
|
316
192
|
}
|
|
317
193
|
}
|
|
318
|
-
await this.
|
|
194
|
+
await this.writeDB.put(doc);
|
|
319
195
|
logger.debug(`Put doc: ${id}`);
|
|
320
196
|
if (this.pendingUpdates[id].length === 0) {
|
|
321
197
|
this.inprogressUpdates[id] = false;
|
|
@@ -341,6 +217,32 @@ var init_updateQueue = __esm({
|
|
|
341
217
|
}
|
|
342
218
|
});
|
|
343
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
|
+
|
|
344
246
|
// src/impl/couch/clientCache.ts
|
|
345
247
|
async function GET_CACHED(k, f) {
|
|
346
248
|
if (CLIENT_CACHE[k]) {
|
|
@@ -364,7 +266,8 @@ var init_clientCache = __esm({
|
|
|
364
266
|
async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = (0, import_common2.blankCourseElo)()) {
|
|
365
267
|
const db = getCourseDB(courseID);
|
|
366
268
|
const payload = (0, import_common3.prepareNote55)(courseID, codeCourse, shape, data, author, tags, uploads);
|
|
367
|
-
const
|
|
269
|
+
const _id = `${DocTypePrefixes["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]}-${(0, import_uuid.v4)()}`;
|
|
270
|
+
const result = await db.put({ ...payload, _id });
|
|
368
271
|
const dataShapeId = import_common.NameSpacer.getDataShapeString({
|
|
369
272
|
course: codeCourse,
|
|
370
273
|
dataShape: shape.name
|
|
@@ -435,7 +338,9 @@ async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags
|
|
|
435
338
|
}
|
|
436
339
|
async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
|
|
437
340
|
const db = getCourseDB(courseID);
|
|
438
|
-
const
|
|
341
|
+
const _id = `${DocTypePrefixes["CARD" /* CARD */]}-${(0, import_uuid.v4)()}`;
|
|
342
|
+
const card = await db.put({
|
|
343
|
+
_id,
|
|
439
344
|
course,
|
|
440
345
|
id_displayable_data,
|
|
441
346
|
id_view,
|
|
@@ -528,7 +433,7 @@ function getCourseDB(courseID) {
|
|
|
528
433
|
pouchDBincludeCredentialsConfig
|
|
529
434
|
);
|
|
530
435
|
}
|
|
531
|
-
var import_common, import_common2, import_common3, AlreadyTaggedErr;
|
|
436
|
+
var import_common, import_common2, import_common3, import_uuid, AlreadyTaggedErr;
|
|
532
437
|
var init_courseAPI = __esm({
|
|
533
438
|
"src/impl/couch/courseAPI.ts"() {
|
|
534
439
|
"use strict";
|
|
@@ -542,6 +447,7 @@ var init_courseAPI = __esm({
|
|
|
542
447
|
import_common3 = require("@vue-skuilder/common");
|
|
543
448
|
init_common();
|
|
544
449
|
init_logger();
|
|
450
|
+
import_uuid = require("uuid");
|
|
545
451
|
AlreadyTaggedErr = class extends Error {
|
|
546
452
|
constructor(message) {
|
|
547
453
|
super(message);
|
|
@@ -811,7 +717,7 @@ async function getCourseQuestionTypes(courseID) {
|
|
|
811
717
|
}
|
|
812
718
|
async function getCourseTagStubs(courseID) {
|
|
813
719
|
logger.debug(`Getting tag stubs for course: ${courseID}`);
|
|
814
|
-
const stubs = await
|
|
720
|
+
const stubs = await filterAllDocsByPrefix(
|
|
815
721
|
getCourseDB2(courseID),
|
|
816
722
|
"TAG" /* TAG */.valueOf() + "-"
|
|
817
723
|
);
|
|
@@ -876,7 +782,7 @@ function getAncestorTagIDs(courseID, tagID) {
|
|
|
876
782
|
}
|
|
877
783
|
}
|
|
878
784
|
async function getChildTagStubs(courseID, tagID) {
|
|
879
|
-
return await
|
|
785
|
+
return await filterAllDocsByPrefix(getCourseDB2(courseID), tagID + ">");
|
|
880
786
|
}
|
|
881
787
|
async function getAppliedTags(id_course, id_card) {
|
|
882
788
|
const db = getCourseDB2(id_course);
|
|
@@ -1383,1217 +1289,1420 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1383
1289
|
}
|
|
1384
1290
|
});
|
|
1385
1291
|
|
|
1386
|
-
// src/impl/couch/
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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"() {
|
|
1390
1307
|
"use strict";
|
|
1391
|
-
|
|
1308
|
+
init_factory();
|
|
1309
|
+
init_logger();
|
|
1310
|
+
import_moment = __toESM(require("moment"));
|
|
1311
|
+
init_pouchdb_setup();
|
|
1392
1312
|
init_couch();
|
|
1393
1313
|
init_courseDB();
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
async getReviewsForcast(daysCount) {
|
|
1405
|
-
const time = import_moment2.default.utc().add(daysCount, "days");
|
|
1406
|
-
return this.getReviewstoDate(time);
|
|
1407
|
-
}
|
|
1408
|
-
async getPendingReviews() {
|
|
1409
|
-
const now = import_moment2.default.utc();
|
|
1410
|
-
return this.getReviewstoDate(now);
|
|
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);
|
|
1411
1324
|
}
|
|
1412
|
-
async
|
|
1413
|
-
|
|
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;
|
|
1414
1336
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
if (crsDoc && crsDoc.settings) {
|
|
1419
|
-
return crsDoc.settings;
|
|
1337
|
+
getContentId(content) {
|
|
1338
|
+
if (content.type === "tag") {
|
|
1339
|
+
return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
|
|
1420
1340
|
} else {
|
|
1421
|
-
|
|
1422
|
-
return {};
|
|
1341
|
+
return `${this._content_prefix}-${content.courseID}`;
|
|
1423
1342
|
}
|
|
1424
1343
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1344
|
+
get ready() {
|
|
1345
|
+
return this._initComplete;
|
|
1427
1346
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
const reviews = await this.user.remote().allDocs({
|
|
1431
|
-
startkey: keys.startkey,
|
|
1432
|
-
endkey: keys.endkey,
|
|
1433
|
-
include_docs: true
|
|
1434
|
-
});
|
|
1435
|
-
logger.debug(
|
|
1436
|
-
`Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
|
|
1437
|
-
);
|
|
1438
|
-
return reviews.rows.filter((r) => {
|
|
1439
|
-
if (r.id.startsWith(REVIEW_PREFIX2)) {
|
|
1440
|
-
const date = import_moment2.default.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
|
|
1441
|
-
if (targetDate.isAfter(date)) {
|
|
1442
|
-
if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
|
|
1443
|
-
return true;
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
}).map((r) => r.doc);
|
|
1347
|
+
getConfig() {
|
|
1348
|
+
return this._cfg;
|
|
1448
1349
|
}
|
|
1449
1350
|
};
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
} catch (e) {
|
|
1459
|
-
const err = e;
|
|
1460
|
-
if (err.status === 404) {
|
|
1461
|
-
await getLocalUserDB(user).put({
|
|
1462
|
-
_id: userClassroomsDoc,
|
|
1463
|
-
registrations: []
|
|
1464
|
-
});
|
|
1465
|
-
ret = await getOrCreateClassroomRegistrationsDoc(user);
|
|
1466
|
-
} else {
|
|
1467
|
-
const errorDetails = {
|
|
1468
|
-
name: err.name,
|
|
1469
|
-
status: err.status,
|
|
1470
|
-
message: err.message,
|
|
1471
|
-
reason: err.reason,
|
|
1472
|
-
error: err.error
|
|
1473
|
-
};
|
|
1474
|
-
logger.error(
|
|
1475
|
-
"Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
|
|
1476
|
-
errorDetails
|
|
1477
|
-
);
|
|
1478
|
-
throw new Error(
|
|
1479
|
-
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
1480
|
-
);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
return ret;
|
|
1484
|
-
}
|
|
1485
|
-
async function getOrCreateCourseRegistrationsDoc(user) {
|
|
1486
|
-
let ret;
|
|
1487
|
-
try {
|
|
1488
|
-
ret = await getLocalUserDB(user).get(userCoursesDoc);
|
|
1489
|
-
} catch (e) {
|
|
1490
|
-
const err = e;
|
|
1491
|
-
if (err.status === 404) {
|
|
1492
|
-
await getLocalUserDB(user).put({
|
|
1493
|
-
_id: userCoursesDoc,
|
|
1494
|
-
courses: [],
|
|
1495
|
-
studyWeight: {}
|
|
1496
|
-
});
|
|
1497
|
-
ret = await getOrCreateCourseRegistrationsDoc(user);
|
|
1498
|
-
} else {
|
|
1499
|
-
throw new Error(
|
|
1500
|
-
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
1501
|
-
);
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
return ret;
|
|
1505
|
-
}
|
|
1506
|
-
async function updateUserElo(user, course_id, elo) {
|
|
1507
|
-
const regDoc = await getOrCreateCourseRegistrationsDoc(user);
|
|
1508
|
-
const course = regDoc.courses.find((c) => c.courseID === course_id);
|
|
1509
|
-
course.elo = elo;
|
|
1510
|
-
return getLocalUserDB(user).put(regDoc);
|
|
1511
|
-
}
|
|
1512
|
-
async function registerUserForClassroom(user, classID, registerAs) {
|
|
1513
|
-
log3(`Registering user: ${user} in course: ${classID}`);
|
|
1514
|
-
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
1515
|
-
const regItem = {
|
|
1516
|
-
classID,
|
|
1517
|
-
registeredAs: registerAs
|
|
1518
|
-
};
|
|
1519
|
-
if (doc.registrations.filter((reg) => {
|
|
1520
|
-
return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
|
|
1521
|
-
}).length === 0) {
|
|
1522
|
-
doc.registrations.push(regItem);
|
|
1523
|
-
} else {
|
|
1524
|
-
log3(`User ${user} is already registered for class ${classID}`);
|
|
1525
|
-
}
|
|
1526
|
-
return getLocalUserDB(user).put(doc);
|
|
1527
|
-
});
|
|
1528
|
-
}
|
|
1529
|
-
async function dropUserFromClassroom(user, classID) {
|
|
1530
|
-
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
1531
|
-
let index = -1;
|
|
1532
|
-
for (let i = 0; i < doc.registrations.length; i++) {
|
|
1533
|
-
if (doc.registrations[i].classID === classID) {
|
|
1534
|
-
index = i;
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
if (index !== -1) {
|
|
1538
|
-
doc.registrations.splice(index, 1);
|
|
1539
|
-
}
|
|
1540
|
-
return getLocalUserDB(user).put(doc);
|
|
1541
|
-
});
|
|
1542
|
-
}
|
|
1543
|
-
async function getUserClassrooms(user) {
|
|
1544
|
-
return getOrCreateClassroomRegistrationsDoc(user);
|
|
1545
|
-
}
|
|
1546
|
-
var import_common6, import_moment3, log3, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
1547
|
-
var init_BaseUserDB = __esm({
|
|
1548
|
-
"src/impl/common/BaseUserDB.ts"() {
|
|
1549
|
-
"use strict";
|
|
1550
|
-
init_util();
|
|
1551
|
-
import_common6 = require("@vue-skuilder/common");
|
|
1552
|
-
import_moment3 = __toESM(require("moment"));
|
|
1553
|
-
init_types_legacy();
|
|
1554
|
-
init_logger();
|
|
1555
|
-
init_userDBHelpers();
|
|
1556
|
-
init_updateQueue();
|
|
1557
|
-
init_user_course_relDB();
|
|
1558
|
-
init_couch();
|
|
1559
|
-
log3 = (s) => {
|
|
1560
|
-
logger.info(s);
|
|
1561
|
-
};
|
|
1562
|
-
cardHistoryPrefix2 = "cardH-";
|
|
1563
|
-
BaseUser = class _BaseUser {
|
|
1564
|
-
static _instance;
|
|
1565
|
-
static _initialized = false;
|
|
1566
|
-
static Dummy(syncStrategy) {
|
|
1567
|
-
return new _BaseUser("Me", syncStrategy);
|
|
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;
|
|
1568
1359
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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
|
+
);
|
|
1366
|
+
try {
|
|
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)}`);
|
|
1378
|
+
}
|
|
1579
1379
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1380
|
+
static async factory(classID, user) {
|
|
1381
|
+
const ret = new _StudentClassroomDB(classID, user);
|
|
1382
|
+
await ret.init();
|
|
1383
|
+
return ret;
|
|
1582
1384
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
return this.remoteDB;
|
|
1385
|
+
setChangeFcn(f) {
|
|
1386
|
+
void this.userMessages.on("change", f);
|
|
1586
1387
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
log3(`Account created successfully, updating username to ${username}`);
|
|
1602
|
-
this._username = username;
|
|
1603
|
-
try {
|
|
1604
|
-
localStorage.removeItem("dbUUID");
|
|
1605
|
-
} catch (e) {
|
|
1606
|
-
logger.warn("localStorage not available (Node.js environment):", e);
|
|
1607
|
-
}
|
|
1608
|
-
await this.init();
|
|
1609
|
-
}
|
|
1610
|
-
return {
|
|
1611
|
-
status: result.status,
|
|
1612
|
-
error: result.error || ""
|
|
1613
|
-
};
|
|
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
|
+
};
|
|
1401
|
+
});
|
|
1614
1402
|
}
|
|
1615
|
-
async
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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));
|
|
1623
1431
|
}
|
|
1624
|
-
logger.warn(`User ${this._username} is already logged in, but executing login again.`);
|
|
1625
1432
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
} catch (e) {
|
|
1633
|
-
logger.warn("localStorage not available (Node.js environment):", e);
|
|
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;
|
|
1634
1439
|
}
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1637
|
-
return loginResult;
|
|
1440
|
+
});
|
|
1638
1441
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
}
|
|
1646
|
-
try {
|
|
1647
|
-
const localDB = getLocalUserDB(this._username);
|
|
1648
|
-
const allDocs = await localDB.allDocs({ include_docs: false });
|
|
1649
|
-
const docsToDelete = allDocs.rows.filter((row) => {
|
|
1650
|
-
const id = row.id;
|
|
1651
|
-
return id.startsWith(cardHistoryPrefix2) || // Card interaction history
|
|
1652
|
-
id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
|
|
1653
|
-
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
1654
|
-
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
1655
|
-
id === _BaseUser.DOC_IDS.CONFIG;
|
|
1656
|
-
}).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
|
|
1657
|
-
if (docsToDelete.length > 0) {
|
|
1658
|
-
await localDB.bulkDocs(docsToDelete);
|
|
1659
|
-
}
|
|
1660
|
-
await this.init();
|
|
1661
|
-
return { status: import_common6.Status.ok };
|
|
1662
|
-
} catch (error) {
|
|
1663
|
-
logger.error("Failed to reset user data:", error);
|
|
1664
|
-
return {
|
|
1665
|
-
status: import_common6.Status.error,
|
|
1666
|
-
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
1667
|
-
};
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
async logout() {
|
|
1671
|
-
if (!this.syncStrategy.canAuthenticate()) {
|
|
1672
|
-
this._username = await this.syncStrategy.getCurrentUsername();
|
|
1673
|
-
await this.init();
|
|
1674
|
-
return { ok: true };
|
|
1675
|
-
}
|
|
1676
|
-
const ret = await this.syncStrategy.logout();
|
|
1677
|
-
this._username = await this.syncStrategy.getCurrentUsername();
|
|
1678
|
-
await this.init();
|
|
1679
|
-
return ret;
|
|
1680
|
-
}
|
|
1681
|
-
update(id, update) {
|
|
1682
|
-
return this.updateQueue.update(id, update);
|
|
1442
|
+
};
|
|
1443
|
+
TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
|
|
1444
|
+
_stuDb;
|
|
1445
|
+
constructor(classID) {
|
|
1446
|
+
super();
|
|
1447
|
+
this._id = classID;
|
|
1683
1448
|
}
|
|
1684
|
-
async
|
|
1685
|
-
|
|
1686
|
-
|
|
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
|
+
);
|
|
1687
1460
|
try {
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1461
|
+
return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
|
|
1462
|
+
this._cfg = cfg;
|
|
1463
|
+
this._initComplete = true;
|
|
1464
|
+
}).then(() => {
|
|
1465
|
+
return;
|
|
1466
|
+
});
|
|
1692
1467
|
} catch (e) {
|
|
1693
|
-
|
|
1694
|
-
if (err.status === 404) {
|
|
1695
|
-
await this.localDB.put({
|
|
1696
|
-
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
1697
|
-
courses: [],
|
|
1698
|
-
studyWeight: {}
|
|
1699
|
-
});
|
|
1700
|
-
ret = await this.getCourseRegistrationsDoc();
|
|
1701
|
-
} else {
|
|
1702
|
-
throw new Error(
|
|
1703
|
-
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
1704
|
-
);
|
|
1705
|
-
}
|
|
1468
|
+
throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
|
|
1706
1469
|
}
|
|
1707
|
-
return ret;
|
|
1708
1470
|
}
|
|
1709
|
-
async
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
});
|
|
1714
|
-
}
|
|
1715
|
-
/**
|
|
1716
|
-
* Returns a promise of the card IDs that the user has
|
|
1717
|
-
* a scheduled review for.
|
|
1718
|
-
*
|
|
1719
|
-
*/
|
|
1720
|
-
async getActiveCards() {
|
|
1721
|
-
const keys = getStartAndEndKeys(REVIEW_PREFIX);
|
|
1722
|
-
const reviews = await this.remoteDB.allDocs({
|
|
1723
|
-
startkey: keys.startkey,
|
|
1724
|
-
endkey: keys.endkey,
|
|
1725
|
-
include_docs: true
|
|
1726
|
-
});
|
|
1727
|
-
return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
|
|
1471
|
+
static async factory(classID) {
|
|
1472
|
+
const ret = new _TeacherClassroomDB(classID);
|
|
1473
|
+
await ret.init();
|
|
1474
|
+
return ret;
|
|
1728
1475
|
}
|
|
1729
|
-
async
|
|
1476
|
+
async removeContent(content) {
|
|
1477
|
+
const contentID = this.getContentId(content);
|
|
1730
1478
|
try {
|
|
1731
|
-
const
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
for (let i = 0; i < hist.length; i++) {
|
|
1739
|
-
try {
|
|
1740
|
-
if (hist[i] && Array.isArray(hist[i].records)) {
|
|
1741
|
-
hist[i].records.forEach((record) => {
|
|
1742
|
-
try {
|
|
1743
|
-
if (!record.timeStamp) {
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
let timeStamp;
|
|
1747
|
-
if (typeof record.timeStamp === "object") {
|
|
1748
|
-
if (typeof record.timeStamp.toDate === "function") {
|
|
1749
|
-
timeStamp = record.timeStamp.toISOString();
|
|
1750
|
-
} else if (record.timeStamp instanceof Date) {
|
|
1751
|
-
timeStamp = record.timeStamp.toISOString();
|
|
1752
|
-
} else {
|
|
1753
|
-
if (sampleCount < 3) {
|
|
1754
|
-
logger.warn("Unknown timestamp object type:", record.timeStamp);
|
|
1755
|
-
sampleCount++;
|
|
1756
|
-
}
|
|
1757
|
-
return;
|
|
1758
|
-
}
|
|
1759
|
-
} else if (typeof record.timeStamp === "string") {
|
|
1760
|
-
const date = new Date(record.timeStamp);
|
|
1761
|
-
if (isNaN(date.getTime())) {
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
timeStamp = record.timeStamp;
|
|
1765
|
-
} else if (typeof record.timeStamp === "number") {
|
|
1766
|
-
timeStamp = new Date(record.timeStamp).toISOString();
|
|
1767
|
-
} else {
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
allRecords.push({
|
|
1771
|
-
timeStamp,
|
|
1772
|
-
courseID: record.courseID || "unknown",
|
|
1773
|
-
cardID: record.cardID || "unknown",
|
|
1774
|
-
timeSpent: record.timeSpent || 0,
|
|
1775
|
-
type: "card_view"
|
|
1776
|
-
});
|
|
1777
|
-
} catch (err) {
|
|
1778
|
-
}
|
|
1779
|
-
});
|
|
1780
|
-
}
|
|
1781
|
-
} catch (err) {
|
|
1782
|
-
logger.error("Error processing history item:", err);
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
logger.debug(`Found ${allRecords.length} activity records`);
|
|
1786
|
-
return allRecords;
|
|
1787
|
-
} catch (err) {
|
|
1788
|
-
logger.error("Error in getActivityRecords:", err);
|
|
1789
|
-
return [];
|
|
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);
|
|
1790
1486
|
}
|
|
1791
1487
|
}
|
|
1792
|
-
async
|
|
1793
|
-
|
|
1794
|
-
const
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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
|
+
});
|
|
1515
|
+
return true;
|
|
1516
|
+
} else {
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
|
|
1522
|
+
skip_setup: true
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
|
|
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
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
// src/core/interfaces/courseDB.ts
|
|
1548
|
+
var init_courseDB2 = __esm({
|
|
1549
|
+
"src/core/interfaces/courseDB.ts"() {
|
|
1550
|
+
"use strict";
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
// src/core/interfaces/dataLayerProvider.ts
|
|
1555
|
+
var init_dataLayerProvider = __esm({
|
|
1556
|
+
"src/core/interfaces/dataLayerProvider.ts"() {
|
|
1557
|
+
"use strict";
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
// src/core/interfaces/userDB.ts
|
|
1562
|
+
var init_userDB = __esm({
|
|
1563
|
+
"src/core/interfaces/userDB.ts"() {
|
|
1564
|
+
"use strict";
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
// src/core/interfaces/index.ts
|
|
1569
|
+
var init_interfaces = __esm({
|
|
1570
|
+
"src/core/interfaces/index.ts"() {
|
|
1571
|
+
"use strict";
|
|
1572
|
+
init_adminDB();
|
|
1573
|
+
init_classroomDB();
|
|
1574
|
+
init_contentSource();
|
|
1575
|
+
init_courseDB2();
|
|
1576
|
+
init_dataLayerProvider();
|
|
1577
|
+
init_userDB();
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
|
|
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}`;
|
|
1591
|
+
}
|
|
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");
|
|
1650
|
+
}
|
|
1651
|
+
function getDbPath(dbName) {
|
|
1652
|
+
return path.join(getAppDataDirectory(), dbName);
|
|
1653
|
+
}
|
|
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;
|
|
1673
|
+
}
|
|
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);
|
|
1684
|
+
}
|
|
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
|
+
});
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
function getLocalUserDB(username) {
|
|
1709
|
+
const dbName = `userdb-${username}`;
|
|
1710
|
+
if (typeof window === "undefined") {
|
|
1711
|
+
return new pouchdb_setup_default(getDbPath(dbName), {});
|
|
1712
|
+
} else {
|
|
1713
|
+
return new pouchdb_setup_default(dbName, {});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
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
|
+
});
|
|
1728
|
+
}
|
|
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)}`);
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
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
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
|
|
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"() {
|
|
1760
|
+
"use strict";
|
|
1761
|
+
import_moment3 = __toESM(require("moment"));
|
|
1762
|
+
init_logger();
|
|
1763
|
+
UsrCrsData = class {
|
|
1764
|
+
user;
|
|
1765
|
+
_courseId;
|
|
1766
|
+
constructor(user, courseId) {
|
|
1767
|
+
this.user = user;
|
|
1768
|
+
this._courseId = courseId;
|
|
1812
1769
|
}
|
|
1813
1770
|
async getReviewsForcast(daysCount) {
|
|
1814
1771
|
const time = import_moment3.default.utc().add(daysCount, "days");
|
|
1815
1772
|
return this.getReviewstoDate(time);
|
|
1816
1773
|
}
|
|
1817
|
-
async getPendingReviews(
|
|
1774
|
+
async getPendingReviews() {
|
|
1818
1775
|
const now = import_moment3.default.utc();
|
|
1819
|
-
return this.getReviewstoDate(now
|
|
1820
|
-
}
|
|
1821
|
-
async getScheduledReviewCount(course_id) {
|
|
1822
|
-
return (await this.getPendingReviews(course_id)).length;
|
|
1776
|
+
return this.getReviewstoDate(now);
|
|
1823
1777
|
}
|
|
1824
|
-
async
|
|
1825
|
-
|
|
1826
|
-
return regDoc.courses.filter((c) => {
|
|
1827
|
-
return !c.status || c.status === "active" || c.status === "maintenance-mode";
|
|
1828
|
-
});
|
|
1778
|
+
async getScheduledReviewCount() {
|
|
1779
|
+
return (await this.getPendingReviews()).length;
|
|
1829
1780
|
}
|
|
1830
|
-
async
|
|
1831
|
-
const
|
|
1832
|
-
const
|
|
1833
|
-
if (
|
|
1834
|
-
return
|
|
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;
|
|
1835
1786
|
} else {
|
|
1836
|
-
|
|
1787
|
+
logger.warn(`no settings found during lookup on course ${this._courseId}`);
|
|
1788
|
+
return {};
|
|
1837
1789
|
}
|
|
1838
1790
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
count: 0
|
|
1853
|
-
},
|
|
1854
|
-
tags: {},
|
|
1855
|
-
misc: {}
|
|
1856
|
-
}
|
|
1857
|
-
};
|
|
1858
|
-
if (doc.courses.filter((course) => {
|
|
1859
|
-
return course.courseID === regItem.courseID;
|
|
1860
|
-
}).length === 0) {
|
|
1861
|
-
log3(`It's a new course registration!`);
|
|
1862
|
-
doc.courses.push(regItem);
|
|
1863
|
-
doc.studyWeight[course_id] = 1;
|
|
1864
|
-
} else {
|
|
1865
|
-
doc.courses.forEach((c) => {
|
|
1866
|
-
log3(`Found the previously registered course!`);
|
|
1867
|
-
if (c.courseID === course_id) {
|
|
1868
|
-
c.status = status;
|
|
1869
|
-
}
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
return this.localDB.put(doc);
|
|
1873
|
-
}).catch((e) => {
|
|
1874
|
-
log3(`Registration failed because of: ${JSON.stringify(e)}`);
|
|
1875
|
-
throw e;
|
|
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);
|
|
1876
1804
|
});
|
|
1877
1805
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1806
|
+
};
|
|
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;
|
|
1892
|
+
}
|
|
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);
|
|
1925
|
+
}
|
|
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;
|
|
1896
1936
|
}
|
|
1897
|
-
|
|
1898
|
-
return
|
|
1937
|
+
isLoggedIn() {
|
|
1938
|
+
return !this._username.startsWith(GuestUsername);
|
|
1899
1939
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
const registeredCourses = await this.getCourseRegistrationsDoc();
|
|
1903
|
-
courseIDs = courseIDs.concat(
|
|
1904
|
-
registeredCourses.courses.map((course) => {
|
|
1905
|
-
return course.courseID;
|
|
1906
|
-
})
|
|
1907
|
-
);
|
|
1908
|
-
const cfgs = await Promise.all(
|
|
1909
|
-
courseIDs.map(async (id) => {
|
|
1910
|
-
return await getCredentialledCourseConfig(id);
|
|
1911
|
-
})
|
|
1912
|
-
);
|
|
1913
|
-
return cfgs;
|
|
1940
|
+
remote() {
|
|
1941
|
+
return this.remoteDB;
|
|
1914
1942
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
logger.debug("Raw config from DB:", cfg);
|
|
1924
|
-
return cfg;
|
|
1925
|
-
} catch (e) {
|
|
1926
|
-
const err = e;
|
|
1927
|
-
if (err.name && err.name === "not_found") {
|
|
1928
|
-
await this.localDB.put(defaultConfig);
|
|
1929
|
-
return this.getConfig();
|
|
1930
|
-
} else {
|
|
1931
|
-
logger.error(`Error setting user default config:`, e);
|
|
1932
|
-
throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
|
|
1933
|
-
}
|
|
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");
|
|
1934
1951
|
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
...c,
|
|
1941
|
-
...items
|
|
1942
|
-
});
|
|
1943
|
-
if (put.ok) {
|
|
1944
|
-
logger.debug(`Config items set: ${JSON.stringify(items)}`);
|
|
1945
|
-
} else {
|
|
1946
|
-
logger.error(`Error setting config items: ${JSON.stringify(put)}`);
|
|
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
|
+
);
|
|
1947
1957
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
*/
|
|
1959
|
-
static async instance(syncStrategy, username) {
|
|
1960
|
-
if (username) {
|
|
1961
|
-
_BaseUser._instance = new _BaseUser(username, syncStrategy);
|
|
1962
|
-
await _BaseUser._instance.init();
|
|
1963
|
-
return _BaseUser._instance;
|
|
1964
|
-
} else if (_BaseUser._instance && _BaseUser._initialized) {
|
|
1965
|
-
return _BaseUser._instance;
|
|
1966
|
-
} else if (_BaseUser._instance) {
|
|
1967
|
-
return new Promise((resolve) => {
|
|
1968
|
-
(function waitForUser() {
|
|
1969
|
-
if (_BaseUser._initialized) {
|
|
1970
|
-
return resolve(_BaseUser._instance);
|
|
1971
|
-
} else {
|
|
1972
|
-
setTimeout(waitForUser, 50);
|
|
1973
|
-
}
|
|
1974
|
-
})();
|
|
1975
|
-
});
|
|
1976
|
-
} else {
|
|
1977
|
-
const guestUsername = await syncStrategy.getCurrentUsername();
|
|
1978
|
-
_BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
|
|
1979
|
-
await _BaseUser._instance.init();
|
|
1980
|
-
return _BaseUser._instance;
|
|
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);
|
|
1966
|
+
}
|
|
1967
|
+
await this.init();
|
|
1981
1968
|
}
|
|
1969
|
+
return {
|
|
1970
|
+
status: result.status,
|
|
1971
|
+
error: result.error || ""
|
|
1972
|
+
};
|
|
1982
1973
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
this.syncStrategy = syncStrategy;
|
|
1987
|
-
this.setDBandQ();
|
|
1988
|
-
}
|
|
1989
|
-
setDBandQ() {
|
|
1990
|
-
this.localDB = getLocalUserDB(this._username);
|
|
1991
|
-
this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
|
|
1992
|
-
this.updateQueue = new UpdateQueue(this.localDB);
|
|
1993
|
-
}
|
|
1994
|
-
async init() {
|
|
1995
|
-
_BaseUser._initialized = false;
|
|
1996
|
-
if (this._username === "admin") {
|
|
1997
|
-
_BaseUser._initialized = true;
|
|
1998
|
-
return;
|
|
1974
|
+
async login(username, password) {
|
|
1975
|
+
if (!this.syncStrategy.canAuthenticate()) {
|
|
1976
|
+
throw new Error("Authentication not supported by current sync strategy");
|
|
1999
1977
|
}
|
|
2000
|
-
this.
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
_BaseUser._initialized = true;
|
|
2005
|
-
}
|
|
2006
|
-
static designDocs = [
|
|
2007
|
-
{
|
|
2008
|
-
_id: "_design/reviewCards",
|
|
2009
|
-
views: {
|
|
2010
|
-
reviewCards: {
|
|
2011
|
-
map: `function (doc) {
|
|
2012
|
-
if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
|
|
2013
|
-
emit(doc._id, doc.courseId + '-' + doc.cardId);
|
|
2014
|
-
}
|
|
2015
|
-
}`
|
|
2016
|
-
}
|
|
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}.`);
|
|
2017
1982
|
}
|
|
1983
|
+
logger.warn(`User ${this._username} is already logged in, but executing login again.`);
|
|
2018
1984
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}
|
|
2024
|
-
for (const doc of _BaseUser.designDocs) {
|
|
1985
|
+
const loginResult = await this.syncStrategy.authenticate(username, password);
|
|
1986
|
+
if (loginResult.ok) {
|
|
1987
|
+
log3(`Logged in as ${username}`);
|
|
1988
|
+
this._username = username;
|
|
2025
1989
|
try {
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
1990
|
+
localStorage.removeItem("dbUUID");
|
|
1991
|
+
} catch (e) {
|
|
1992
|
+
logger.warn("localStorage not available (Node.js environment):", e);
|
|
1993
|
+
}
|
|
1994
|
+
await this.init();
|
|
1995
|
+
}
|
|
1996
|
+
return loginResult;
|
|
1997
|
+
}
|
|
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
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
try {
|
|
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);
|
|
2048
2018
|
}
|
|
2019
|
+
await this.init();
|
|
2020
|
+
return { status: import_common7.Status.ok };
|
|
2021
|
+
} catch (error) {
|
|
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
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
async logout() {
|
|
2030
|
+
if (!this.syncStrategy.canAuthenticate()) {
|
|
2031
|
+
this._username = await this.syncStrategy.getCurrentUsername();
|
|
2032
|
+
await this.init();
|
|
2033
|
+
return { ok: true };
|
|
2049
2034
|
}
|
|
2035
|
+
const ret = await this.syncStrategy.logout();
|
|
2036
|
+
this._username = await this.syncStrategy.getCurrentUsername();
|
|
2037
|
+
await this.init();
|
|
2038
|
+
return ret;
|
|
2050
2039
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2040
|
+
update(id, update) {
|
|
2041
|
+
return this.updateQueue.update(id, update);
|
|
2042
|
+
}
|
|
2043
|
+
async getCourseRegistrationsDoc() {
|
|
2044
|
+
logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
|
|
2045
|
+
let ret;
|
|
2053
2046
|
try {
|
|
2054
|
-
const
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
});
|
|
2047
|
+
const regDoc = await this.localDB.get(
|
|
2048
|
+
_BaseUser.DOC_IDS.COURSE_REGISTRATIONS
|
|
2049
|
+
);
|
|
2050
|
+
return regDoc;
|
|
2059
2051
|
} catch (e) {
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2052
|
+
const err = e;
|
|
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
|
+
);
|
|
2063
2064
|
}
|
|
2064
|
-
throw e;
|
|
2065
2065
|
}
|
|
2066
|
+
return ret;
|
|
2067
|
+
}
|
|
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
|
+
});
|
|
2066
2073
|
}
|
|
2067
2074
|
/**
|
|
2068
|
-
*
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2071
|
-
* // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
|
|
2075
|
+
* Returns a promise of the card IDs that the user has
|
|
2076
|
+
* a scheduled review for.
|
|
2072
2077
|
*
|
|
2073
|
-
* @param record the recent recorded interaction between user and card
|
|
2074
|
-
* @returns The updated state of the card's CardHistory data
|
|
2075
2078
|
*/
|
|
2076
|
-
async
|
|
2077
|
-
const
|
|
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}`);
|
|
2087
|
+
}
|
|
2088
|
+
async getActivityRecords() {
|
|
2079
2089
|
try {
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
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);
|
|
2088
2142
|
}
|
|
2089
|
-
);
|
|
2090
|
-
cardHistory.records = cardHistory.records.map((record2) => {
|
|
2091
|
-
const ret = {
|
|
2092
|
-
...record2
|
|
2093
|
-
};
|
|
2094
|
-
ret.timeStamp = import_moment3.default.utc(record2.timeStamp);
|
|
2095
|
-
return ret;
|
|
2096
|
-
});
|
|
2097
|
-
return cardHistory;
|
|
2098
|
-
} catch (e) {
|
|
2099
|
-
const reason = e;
|
|
2100
|
-
if (reason.status === 404) {
|
|
2101
|
-
const initCardHistory = {
|
|
2102
|
-
_id: cardHistoryID,
|
|
2103
|
-
cardID: record.cardID,
|
|
2104
|
-
courseID: record.courseID,
|
|
2105
|
-
records: [record],
|
|
2106
|
-
lapses: 0,
|
|
2107
|
-
streak: 0,
|
|
2108
|
-
bestInterval: 0
|
|
2109
|
-
};
|
|
2110
|
-
void this.remoteDB.put(initCardHistory);
|
|
2111
|
-
return initCardHistory;
|
|
2112
|
-
} else {
|
|
2113
|
-
throw new Error(`putCardRecord failed because of:
|
|
2114
|
-
name:${reason.name}
|
|
2115
|
-
error: ${reason.error}
|
|
2116
|
-
message: ${reason.message}`);
|
|
2117
2143
|
}
|
|
2144
|
+
logger.debug(`Found ${allRecords.length} activity records`);
|
|
2145
|
+
return allRecords;
|
|
2146
|
+
} catch (err) {
|
|
2147
|
+
logger.error("Error in getActivityRecords:", err);
|
|
2148
|
+
return [];
|
|
2118
2149
|
}
|
|
2119
2150
|
}
|
|
2120
|
-
async
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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
|
+
}
|
|
2139
2171
|
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2172
|
+
}
|
|
2173
|
+
}).map((r) => r.doc);
|
|
2174
|
+
}
|
|
2175
|
+
async getReviewsForcast(daysCount) {
|
|
2176
|
+
const time = import_moment4.default.utc().add(daysCount, "days");
|
|
2177
|
+
return this.getReviewstoDate(time);
|
|
2178
|
+
}
|
|
2179
|
+
async getPendingReviews(course_id) {
|
|
2180
|
+
const now = import_moment4.default.utc();
|
|
2181
|
+
return this.getReviewstoDate(now, course_id);
|
|
2182
|
+
}
|
|
2183
|
+
async getScheduledReviewCount(course_id) {
|
|
2184
|
+
return (await this.getPendingReviews(course_id)).length;
|
|
2185
|
+
}
|
|
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
|
+
});
|
|
2191
|
+
}
|
|
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}`);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
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;
|
|
2150
2231
|
}
|
|
2151
2232
|
});
|
|
2152
|
-
await Promise.all(deletePromises);
|
|
2153
|
-
log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
|
|
2154
|
-
} else {
|
|
2155
|
-
log3("No duplicate reviews found");
|
|
2156
|
-
}
|
|
2157
|
-
} catch (error) {
|
|
2158
|
-
log3(`Error during review deduplication: ${error}`);
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
/**
|
|
2162
|
-
* Returns a promise of the card IDs that the user has
|
|
2163
|
-
* encountered in the past.
|
|
2164
|
-
*
|
|
2165
|
-
* @param course_id optional specification of individual course
|
|
2166
|
-
*/
|
|
2167
|
-
async getSeenCards(course_id) {
|
|
2168
|
-
let prefix = cardHistoryPrefix2;
|
|
2169
|
-
if (course_id) {
|
|
2170
|
-
prefix += course_id;
|
|
2171
|
-
}
|
|
2172
|
-
const docs = await filterAllDocsByPrefix(this.localDB, prefix, {
|
|
2173
|
-
include_docs: false
|
|
2174
|
-
});
|
|
2175
|
-
const ret = [];
|
|
2176
|
-
docs.rows.forEach((row) => {
|
|
2177
|
-
if (row.id.startsWith(cardHistoryPrefix2)) {
|
|
2178
|
-
ret.push(row.id.substr(cardHistoryPrefix2.length));
|
|
2179
2233
|
}
|
|
2234
|
+
return this.localDB.put(doc);
|
|
2235
|
+
}).catch((e) => {
|
|
2236
|
+
log3(`Registration failed because of: ${JSON.stringify(e)}`);
|
|
2237
|
+
throw e;
|
|
2180
2238
|
});
|
|
2181
|
-
return ret;
|
|
2182
|
-
}
|
|
2183
|
-
/**
|
|
2184
|
-
*
|
|
2185
|
-
* @returns A promise of the cards that the user has seen in the past.
|
|
2186
|
-
*/
|
|
2187
|
-
async getHistory() {
|
|
2188
|
-
const cards = await filterAllDocsByPrefix(
|
|
2189
|
-
this.remoteDB,
|
|
2190
|
-
cardHistoryPrefix2,
|
|
2191
|
-
{
|
|
2192
|
-
include_docs: true,
|
|
2193
|
-
attachments: false
|
|
2194
|
-
}
|
|
2195
|
-
);
|
|
2196
|
-
return cards.rows.map((r) => r.doc);
|
|
2197
2239
|
}
|
|
2198
|
-
async
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
if (
|
|
2203
|
-
|
|
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;
|
|
2204
2246
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2247
|
+
}
|
|
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
|
+
);
|
|
2208
2255
|
}
|
|
2209
2256
|
return this.localDB.put(doc);
|
|
2210
2257
|
});
|
|
2211
2258
|
}
|
|
2212
|
-
async
|
|
2213
|
-
|
|
2214
|
-
const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
|
|
2215
|
-
if (crsDoc) {
|
|
2216
|
-
return crsDoc.settings;
|
|
2217
|
-
} else {
|
|
2218
|
-
throw new Error(`getCourseSettings Failed:
|
|
2219
|
-
User is not registered for course ${course_id}`);
|
|
2220
|
-
}
|
|
2259
|
+
async getCourseInterface(courseId) {
|
|
2260
|
+
return new UsrCrsData(this, courseId);
|
|
2221
2261
|
}
|
|
2222
|
-
async
|
|
2223
|
-
let
|
|
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;
|
|
2276
|
+
}
|
|
2277
|
+
async getConfig() {
|
|
2278
|
+
const defaultConfig = {
|
|
2279
|
+
_id: _BaseUser.DOC_IDS.CONFIG,
|
|
2280
|
+
darkMode: false,
|
|
2281
|
+
likesConfetti: false,
|
|
2282
|
+
sessionTimeLimit: 5
|
|
2283
|
+
};
|
|
2224
2284
|
try {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2285
|
+
const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
|
|
2286
|
+
logger.debug("Raw config from DB:", cfg);
|
|
2287
|
+
return cfg;
|
|
2228
2288
|
} catch (e) {
|
|
2229
2289
|
const err = e;
|
|
2230
|
-
if (err.
|
|
2231
|
-
await this.
|
|
2232
|
-
|
|
2233
|
-
registrations: []
|
|
2234
|
-
});
|
|
2235
|
-
ret = await this.getOrCreateClassroomRegistrationsDoc();
|
|
2290
|
+
if (err.name && err.name === "not_found") {
|
|
2291
|
+
await this.localDB.put(defaultConfig);
|
|
2292
|
+
return this.getConfig();
|
|
2236
2293
|
} else {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
status: err.status,
|
|
2240
|
-
message: err.message,
|
|
2241
|
-
reason: err.reason,
|
|
2242
|
-
error: err.error
|
|
2243
|
-
};
|
|
2244
|
-
logger.error(
|
|
2245
|
-
"Database error in getOrCreateClassroomRegistrationsDoc (private method):",
|
|
2246
|
-
errorDetails
|
|
2247
|
-
);
|
|
2248
|
-
throw new Error(
|
|
2249
|
-
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
2250
|
-
);
|
|
2294
|
+
logger.error(`Error setting user default config:`, e);
|
|
2295
|
+
throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
|
|
2251
2296
|
}
|
|
2252
2297
|
}
|
|
2253
|
-
logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
|
|
2254
|
-
return ret;
|
|
2255
|
-
}
|
|
2256
|
-
/**
|
|
2257
|
-
* Retrieves the list of active classroom IDs where the user is registered as a student.
|
|
2258
|
-
*
|
|
2259
|
-
* @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
|
|
2260
|
-
* registration document is unavailable due to database errors
|
|
2261
|
-
*
|
|
2262
|
-
* @description This method gracefully handles database connectivity issues by returning
|
|
2263
|
-
* an empty array when the classroom registrations document cannot be accessed.
|
|
2264
|
-
* This ensures that users can still access other application features even
|
|
2265
|
-
* when classroom functionality is temporarily unavailable.
|
|
2266
|
-
*/
|
|
2267
|
-
async getActiveClasses() {
|
|
2268
|
-
try {
|
|
2269
|
-
return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
|
|
2270
|
-
} catch (error) {
|
|
2271
|
-
logger.warn(
|
|
2272
|
-
"Failed to load classroom registrations, continuing without classroom data:",
|
|
2273
|
-
error
|
|
2274
|
-
);
|
|
2275
|
-
return [];
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
async scheduleCardReview(review) {
|
|
2279
|
-
return scheduleCardReviewLocal(this.remoteDB, review);
|
|
2280
|
-
}
|
|
2281
|
-
async removeScheduledCardReview(reviewId) {
|
|
2282
|
-
return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
|
|
2283
|
-
}
|
|
2284
|
-
async registerForClassroom(_classId, _registerAs) {
|
|
2285
|
-
return registerUserForClassroom(this._username, _classId, _registerAs);
|
|
2286
|
-
}
|
|
2287
|
-
async dropFromClassroom(classId) {
|
|
2288
|
-
return dropUserFromClassroom(this._username, classId);
|
|
2289
|
-
}
|
|
2290
|
-
async getUserClassrooms() {
|
|
2291
|
-
return getUserClassrooms(this._username);
|
|
2292
|
-
}
|
|
2293
|
-
async updateUserElo(courseId, elo) {
|
|
2294
|
-
return updateUserElo(this._username, courseId, elo);
|
|
2295
|
-
}
|
|
2296
|
-
};
|
|
2297
|
-
userCoursesDoc = "CourseRegistrations";
|
|
2298
|
-
userClassroomsDoc = "ClassroomRegistrations";
|
|
2299
|
-
}
|
|
2300
|
-
});
|
|
2301
|
-
|
|
2302
|
-
// src/impl/common/index.ts
|
|
2303
|
-
var init_common = __esm({
|
|
2304
|
-
"src/impl/common/index.ts"() {
|
|
2305
|
-
"use strict";
|
|
2306
|
-
init_SyncStrategy();
|
|
2307
|
-
init_BaseUserDB();
|
|
2308
|
-
init_userDBHelpers();
|
|
2309
|
-
}
|
|
2310
|
-
});
|
|
2311
|
-
|
|
2312
|
-
// src/factory.ts
|
|
2313
|
-
function getDataLayer() {
|
|
2314
|
-
if (!dataLayerInstance) {
|
|
2315
|
-
throw new Error("Data layer not initialized. Call initializeDataLayer first.");
|
|
2316
|
-
}
|
|
2317
|
-
return dataLayerInstance;
|
|
2318
|
-
}
|
|
2319
|
-
var ENV, dataLayerInstance;
|
|
2320
|
-
var init_factory = __esm({
|
|
2321
|
-
"src/factory.ts"() {
|
|
2322
|
-
"use strict";
|
|
2323
|
-
init_common();
|
|
2324
|
-
init_logger();
|
|
2325
|
-
ENV = {
|
|
2326
|
-
COUCHDB_SERVER_PROTOCOL: "NOT_SET",
|
|
2327
|
-
COUCHDB_SERVER_URL: "NOT_SET"
|
|
2328
|
-
};
|
|
2329
|
-
dataLayerInstance = null;
|
|
2330
|
-
}
|
|
2331
|
-
});
|
|
2332
|
-
|
|
2333
|
-
// src/impl/couch/classroomDB.ts
|
|
2334
|
-
function getClassroomDB(classID, version) {
|
|
2335
|
-
const dbName = `classdb-${version}-${classID}`;
|
|
2336
|
-
logger.info(`Retrieving classroom db: ${dbName}`);
|
|
2337
|
-
return new pouchdb_setup_default(
|
|
2338
|
-
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2339
|
-
pouchDBincludeCredentialsConfig
|
|
2340
|
-
);
|
|
2341
|
-
}
|
|
2342
|
-
async function getClassroomConfig(classID) {
|
|
2343
|
-
return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
|
|
2344
|
-
}
|
|
2345
|
-
var import_moment4, classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
|
|
2346
|
-
var init_classroomDB = __esm({
|
|
2347
|
-
"src/impl/couch/classroomDB.ts"() {
|
|
2348
|
-
"use strict";
|
|
2349
|
-
init_factory();
|
|
2350
|
-
init_logger();
|
|
2351
|
-
import_moment4 = __toESM(require("moment"));
|
|
2352
|
-
init_pouchdb_setup();
|
|
2353
|
-
init_couch();
|
|
2354
|
-
init_courseDB();
|
|
2355
|
-
classroomLookupDBTitle = "classdb-lookup";
|
|
2356
|
-
CLASSROOM_CONFIG = "ClassroomConfig";
|
|
2357
|
-
ClassroomDBBase = class {
|
|
2358
|
-
_id;
|
|
2359
|
-
_db;
|
|
2360
|
-
_cfg;
|
|
2361
|
-
_initComplete = false;
|
|
2362
|
-
_content_prefix = "content";
|
|
2363
|
-
get _content_searchkeys() {
|
|
2364
|
-
return getStartAndEndKeys2(this._content_prefix);
|
|
2365
|
-
}
|
|
2366
|
-
async getAssignedContent() {
|
|
2367
|
-
logger.info(`Getting assigned content...`);
|
|
2368
|
-
const docRows = await this._db.allDocs({
|
|
2369
|
-
startkey: this._content_prefix,
|
|
2370
|
-
endkey: this._content_prefix + `\uFFF0`,
|
|
2371
|
-
include_docs: true
|
|
2372
|
-
});
|
|
2373
|
-
const ret = docRows.rows.map((row) => {
|
|
2374
|
-
return row.doc;
|
|
2375
|
-
});
|
|
2376
|
-
return ret;
|
|
2377
2298
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
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)}`);
|
|
2381
2308
|
} else {
|
|
2382
|
-
|
|
2309
|
+
logger.error(`Error setting config items: ${JSON.stringify(put)}`);
|
|
2383
2310
|
}
|
|
2384
2311
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
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
|
+
}
|
|
2387
2345
|
}
|
|
2388
|
-
|
|
2389
|
-
|
|
2346
|
+
constructor(username, syncStrategy) {
|
|
2347
|
+
_BaseUser._initialized = false;
|
|
2348
|
+
this._username = username;
|
|
2349
|
+
this.syncStrategy = syncStrategy;
|
|
2350
|
+
this.setDBandQ();
|
|
2390
2351
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
constructor(classID, user) {
|
|
2397
|
-
super();
|
|
2398
|
-
this._id = classID;
|
|
2399
|
-
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);
|
|
2400
2357
|
}
|
|
2401
2358
|
async init() {
|
|
2402
|
-
|
|
2403
|
-
this.
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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) {
|
|
2407
2417
|
try {
|
|
2408
|
-
const
|
|
2409
|
-
this.
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
live: true,
|
|
2413
|
-
include_docs: true
|
|
2418
|
+
const existingDoc = await this.remoteDB.get(doc._id);
|
|
2419
|
+
await this.remoteDB.put({
|
|
2420
|
+
...doc,
|
|
2421
|
+
_rev: existingDoc._rev
|
|
2414
2422
|
});
|
|
2415
|
-
this._initComplete = true;
|
|
2416
|
-
return;
|
|
2417
2423
|
} catch (e) {
|
|
2418
|
-
|
|
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;
|
|
2419
2429
|
}
|
|
2420
2430
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
+
}
|
|
2425
2483
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
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
|
+
}
|
|
2428
2524
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
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
|
+
}
|
|
2442
2544
|
});
|
|
2545
|
+
return ret;
|
|
2443
2546
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
ret = ret.concat(await db.getNewCards());
|
|
2456
|
-
} else if (content.type === "tag") {
|
|
2457
|
-
const tagDoc = await getTag(content.courseID, content.tagID);
|
|
2458
|
-
ret = ret.concat(
|
|
2459
|
-
tagDoc.taggedCards.map((c) => {
|
|
2460
|
-
return {
|
|
2461
|
-
courseID: content.courseID,
|
|
2462
|
-
cardID: c,
|
|
2463
|
-
qualifiedID: `${content.courseID}-${c}`,
|
|
2464
|
-
contentSourceType: "classroom",
|
|
2465
|
-
contentSourceID: this._id,
|
|
2466
|
-
status: "new"
|
|
2467
|
-
};
|
|
2468
|
-
})
|
|
2469
|
-
);
|
|
2470
|
-
} else if (content.type === "card") {
|
|
2471
|
-
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
|
|
2472
2558
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
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
|
+
});
|
|
2480
2572
|
}
|
|
2573
|
+
return this.localDB.put(doc);
|
|
2481
2574
|
});
|
|
2482
2575
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
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
|
+
}
|
|
2489
2585
|
}
|
|
2490
|
-
async
|
|
2491
|
-
|
|
2492
|
-
const stuDbName = `classdb-student-${this._id}`;
|
|
2493
|
-
this._db = new pouchdb_setup_default(
|
|
2494
|
-
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2495
|
-
pouchDBincludeCredentialsConfig
|
|
2496
|
-
);
|
|
2497
|
-
this._stuDb = new pouchdb_setup_default(
|
|
2498
|
-
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
|
|
2499
|
-
pouchDBincludeCredentialsConfig
|
|
2500
|
-
);
|
|
2586
|
+
async getOrCreateClassroomRegistrationsDoc() {
|
|
2587
|
+
let ret;
|
|
2501
2588
|
try {
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
}).then(() => {
|
|
2506
|
-
return;
|
|
2507
|
-
});
|
|
2589
|
+
ret = await this.remoteDB.get(
|
|
2590
|
+
_BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
|
|
2591
|
+
);
|
|
2508
2592
|
} catch (e) {
|
|
2509
|
-
|
|
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
|
+
}
|
|
2510
2616
|
}
|
|
2511
|
-
|
|
2512
|
-
static async factory(classID) {
|
|
2513
|
-
const ret = new _TeacherClassroomDB(classID);
|
|
2514
|
-
await ret.init();
|
|
2617
|
+
logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
|
|
2515
2618
|
return ret;
|
|
2516
2619
|
}
|
|
2517
|
-
|
|
2518
|
-
|
|
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() {
|
|
2519
2632
|
try {
|
|
2520
|
-
|
|
2521
|
-
await this._db.remove(doc);
|
|
2522
|
-
void this._db.replicate.to(this._stuDb, {
|
|
2523
|
-
doc_ids: [contentID]
|
|
2524
|
-
});
|
|
2633
|
+
return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
|
|
2525
2634
|
} catch (error) {
|
|
2526
|
-
logger.
|
|
2635
|
+
logger.warn(
|
|
2636
|
+
"Failed to load classroom registrations, continuing without classroom data:",
|
|
2637
|
+
error
|
|
2638
|
+
);
|
|
2639
|
+
return [];
|
|
2527
2640
|
}
|
|
2528
2641
|
}
|
|
2529
|
-
async
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
_id: id,
|
|
2547
|
-
assignedBy: content.assignedBy,
|
|
2548
|
-
assignedOn: import_moment4.default.utc(),
|
|
2549
|
-
activeOn: content.activeOn || import_moment4.default.utc()
|
|
2550
|
-
});
|
|
2551
|
-
}
|
|
2552
|
-
if (put.ok) {
|
|
2553
|
-
void this._db.replicate.to(this._stuDb, {
|
|
2554
|
-
doc_ids: [id]
|
|
2555
|
-
});
|
|
2556
|
-
return true;
|
|
2557
|
-
} else {
|
|
2558
|
-
return false;
|
|
2559
|
-
}
|
|
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);
|
|
2560
2659
|
}
|
|
2561
2660
|
};
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
});
|
|
2661
|
+
userCoursesDoc = "CourseRegistrations";
|
|
2662
|
+
userClassroomsDoc = "ClassroomRegistrations";
|
|
2565
2663
|
}
|
|
2566
2664
|
});
|
|
2567
2665
|
|
|
2568
|
-
// src/
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
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();
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
|
|
2676
|
+
// src/factory.ts
|
|
2677
|
+
function getDataLayer() {
|
|
2678
|
+
if (!dataLayerInstance) {
|
|
2679
|
+
throw new Error("Data layer not initialized. Call initializeDataLayer first.");
|
|
2578
2680
|
}
|
|
2681
|
+
return dataLayerInstance;
|
|
2579
2682
|
}
|
|
2580
|
-
var
|
|
2581
|
-
|
|
2683
|
+
var ENV, dataLayerInstance;
|
|
2684
|
+
var init_factory = __esm({
|
|
2685
|
+
"src/factory.ts"() {
|
|
2582
2686
|
"use strict";
|
|
2583
|
-
|
|
2584
|
-
|
|
2687
|
+
init_common();
|
|
2688
|
+
init_logger();
|
|
2689
|
+
ENV = {
|
|
2690
|
+
COUCHDB_SERVER_PROTOCOL: "NOT_SET",
|
|
2691
|
+
COUCHDB_SERVER_URL: "NOT_SET"
|
|
2692
|
+
};
|
|
2693
|
+
dataLayerInstance = null;
|
|
2585
2694
|
}
|
|
2586
2695
|
});
|
|
2587
2696
|
|
|
2588
2697
|
// src/impl/couch/adminDB.ts
|
|
2589
2698
|
var AdminDB;
|
|
2590
|
-
var
|
|
2699
|
+
var init_adminDB2 = __esm({
|
|
2591
2700
|
"src/impl/couch/adminDB.ts"() {
|
|
2592
2701
|
"use strict";
|
|
2593
2702
|
init_pouchdb_setup();
|
|
2594
2703
|
init_factory();
|
|
2595
2704
|
init_couch();
|
|
2596
|
-
|
|
2705
|
+
init_classroomDB2();
|
|
2597
2706
|
init_courseLookupDB();
|
|
2598
2707
|
init_logger();
|
|
2599
2708
|
AdminDB = class {
|
|
@@ -2607,7 +2716,7 @@ var init_adminDB = __esm({
|
|
|
2607
2716
|
async getUsers() {
|
|
2608
2717
|
return (await this.usersDB.allDocs({
|
|
2609
2718
|
include_docs: true,
|
|
2610
|
-
...
|
|
2719
|
+
...getStartAndEndKeys("org.couchdb.user:")
|
|
2611
2720
|
})).rows.map((r) => r.doc);
|
|
2612
2721
|
}
|
|
2613
2722
|
async getCourses() {
|
|
@@ -2700,14 +2809,14 @@ var init_auth = __esm({
|
|
|
2700
2809
|
});
|
|
2701
2810
|
|
|
2702
2811
|
// src/impl/couch/CouchDBSyncStrategy.ts
|
|
2703
|
-
var
|
|
2812
|
+
var import_common9, log4, CouchDBSyncStrategy;
|
|
2704
2813
|
var init_CouchDBSyncStrategy = __esm({
|
|
2705
2814
|
"src/impl/couch/CouchDBSyncStrategy.ts"() {
|
|
2706
2815
|
"use strict";
|
|
2707
2816
|
init_factory();
|
|
2708
2817
|
init_types_legacy();
|
|
2709
2818
|
init_logger();
|
|
2710
|
-
|
|
2819
|
+
import_common9 = require("@vue-skuilder/common");
|
|
2711
2820
|
init_common();
|
|
2712
2821
|
init_pouchdb_setup();
|
|
2713
2822
|
init_couch();
|
|
@@ -2725,6 +2834,13 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2725
2834
|
return this.getUserDB(username);
|
|
2726
2835
|
}
|
|
2727
2836
|
}
|
|
2837
|
+
getWriteDB(username) {
|
|
2838
|
+
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
2839
|
+
return getLocalUserDB(username);
|
|
2840
|
+
} else {
|
|
2841
|
+
return this.getUserDB(username);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2728
2844
|
startSync(localDB, remoteDB) {
|
|
2729
2845
|
if (localDB !== remoteDB) {
|
|
2730
2846
|
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
@@ -2759,32 +2875,32 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2759
2875
|
log4(`CREATEACCOUNT: logged in as new user: ${loginResult.ok}`);
|
|
2760
2876
|
if (loginResult.ok) {
|
|
2761
2877
|
return {
|
|
2762
|
-
status:
|
|
2878
|
+
status: import_common9.Status.ok,
|
|
2763
2879
|
error: void 0
|
|
2764
2880
|
};
|
|
2765
2881
|
} else {
|
|
2766
2882
|
return {
|
|
2767
|
-
status:
|
|
2883
|
+
status: import_common9.Status.error,
|
|
2768
2884
|
error: "Failed to log in after account creation"
|
|
2769
2885
|
};
|
|
2770
2886
|
}
|
|
2771
2887
|
} else {
|
|
2772
2888
|
logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
|
|
2773
2889
|
return {
|
|
2774
|
-
status:
|
|
2890
|
+
status: import_common9.Status.error,
|
|
2775
2891
|
error: "Account creation failed"
|
|
2776
2892
|
};
|
|
2777
2893
|
}
|
|
2778
2894
|
} catch (e) {
|
|
2779
2895
|
if (e.reason === "Document update conflict.") {
|
|
2780
2896
|
return {
|
|
2781
|
-
status:
|
|
2897
|
+
status: import_common9.Status.error,
|
|
2782
2898
|
error: "This username is taken!"
|
|
2783
2899
|
};
|
|
2784
2900
|
}
|
|
2785
2901
|
logger.error(`Error on signup: ${JSON.stringify(e)}`);
|
|
2786
2902
|
return {
|
|
2787
|
-
status:
|
|
2903
|
+
status: import_common9.Status.error,
|
|
2788
2904
|
error: e.message || "Unknown error during account creation"
|
|
2789
2905
|
};
|
|
2790
2906
|
}
|
|
@@ -2878,15 +2994,14 @@ __export(couch_exports, {
|
|
|
2878
2994
|
CouchDBSyncStrategy: () => CouchDBSyncStrategy,
|
|
2879
2995
|
CourseDB: () => CourseDB,
|
|
2880
2996
|
CoursesDB: () => CoursesDB,
|
|
2881
|
-
|
|
2882
|
-
REVIEW_TIME_FORMAT: () => REVIEW_TIME_FORMAT2,
|
|
2997
|
+
REVIEW_TIME_FORMAT: () => REVIEW_TIME_FORMAT,
|
|
2883
2998
|
StudentClassroomDB: () => StudentClassroomDB,
|
|
2884
2999
|
TeacherClassroomDB: () => TeacherClassroomDB,
|
|
2885
3000
|
addNote55: () => addNote55,
|
|
2886
3001
|
addTagToCard: () => addTagToCard,
|
|
2887
3002
|
createTag: () => createTag,
|
|
2888
3003
|
deleteTag: () => deleteTag,
|
|
2889
|
-
filterAllDocsByPrefix: () =>
|
|
3004
|
+
filterAllDocsByPrefix: () => filterAllDocsByPrefix,
|
|
2890
3005
|
getAncestorTagIDs: () => getAncestorTagIDs,
|
|
2891
3006
|
getAppliedTags: () => getAppliedTags,
|
|
2892
3007
|
getChildTagStubs: () => getChildTagStubs,
|
|
@@ -2903,7 +3018,7 @@ __export(couch_exports, {
|
|
|
2903
3018
|
getCredentialledDataShapes: () => getCredentialledDataShapes,
|
|
2904
3019
|
getLatestVersion: () => getLatestVersion,
|
|
2905
3020
|
getRandomCards: () => getRandomCards,
|
|
2906
|
-
getStartAndEndKeys: () =>
|
|
3021
|
+
getStartAndEndKeys: () => getStartAndEndKeys,
|
|
2907
3022
|
getStudySource: () => getStudySource,
|
|
2908
3023
|
getTag: () => getTag,
|
|
2909
3024
|
getTagID: () => getTagID,
|
|
@@ -3031,16 +3146,16 @@ function scheduleCardReview(review) {
|
|
|
3031
3146
|
const now = import_moment5.default.utc();
|
|
3032
3147
|
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
3033
3148
|
void getCouchUserDB(review.user).put({
|
|
3034
|
-
_id:
|
|
3149
|
+
_id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
|
|
3035
3150
|
cardId: review.card_id,
|
|
3036
|
-
reviewTime: review.time,
|
|
3151
|
+
reviewTime: review.time.toISOString(),
|
|
3037
3152
|
courseId: review.course_id,
|
|
3038
|
-
scheduledAt: now,
|
|
3153
|
+
scheduledAt: now.toISOString(),
|
|
3039
3154
|
scheduledFor: review.scheduledFor,
|
|
3040
3155
|
schedulingAgentId: review.schedulingAgentId
|
|
3041
3156
|
});
|
|
3042
3157
|
}
|
|
3043
|
-
function
|
|
3158
|
+
function filterAllDocsByPrefix(db, prefix, opts) {
|
|
3044
3159
|
const options = {
|
|
3045
3160
|
startkey: prefix,
|
|
3046
3161
|
endkey: prefix + "\uFFF0",
|
|
@@ -3051,13 +3166,13 @@ function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
|
3051
3166
|
}
|
|
3052
3167
|
return db.allDocs(options);
|
|
3053
3168
|
}
|
|
3054
|
-
function
|
|
3169
|
+
function getStartAndEndKeys(key) {
|
|
3055
3170
|
return {
|
|
3056
3171
|
startkey: key,
|
|
3057
3172
|
endkey: key + "\uFFF0"
|
|
3058
3173
|
};
|
|
3059
3174
|
}
|
|
3060
|
-
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;
|
|
3061
3176
|
var init_couch = __esm({
|
|
3062
3177
|
"src/impl/couch/index.ts"() {
|
|
3063
3178
|
init_factory();
|
|
@@ -3067,8 +3182,8 @@ var init_couch = __esm({
|
|
|
3067
3182
|
init_pouchdb_setup();
|
|
3068
3183
|
import_process = __toESM(require("process"));
|
|
3069
3184
|
init_contentSource();
|
|
3070
|
-
|
|
3071
|
-
|
|
3185
|
+
init_adminDB2();
|
|
3186
|
+
init_classroomDB2();
|
|
3072
3187
|
init_courseAPI();
|
|
3073
3188
|
init_courseDB();
|
|
3074
3189
|
init_CouchDBSyncStrategy();
|
|
@@ -3085,8 +3200,7 @@ var init_couch = __esm({
|
|
|
3085
3200
|
return pouchdb_setup_default.fetch(url, opts);
|
|
3086
3201
|
}
|
|
3087
3202
|
};
|
|
3088
|
-
|
|
3089
|
-
REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
3203
|
+
REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
3090
3204
|
}
|
|
3091
3205
|
});
|
|
3092
3206
|
init_couch();
|
|
@@ -3098,7 +3212,6 @@ init_couch();
|
|
|
3098
3212
|
CouchDBSyncStrategy,
|
|
3099
3213
|
CourseDB,
|
|
3100
3214
|
CoursesDB,
|
|
3101
|
-
REVIEW_PREFIX,
|
|
3102
3215
|
REVIEW_TIME_FORMAT,
|
|
3103
3216
|
StudentClassroomDB,
|
|
3104
3217
|
TeacherClassroomDB,
|