@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
|
@@ -20,6 +20,20 @@ var init_SyncStrategy = __esm({
|
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
// src/core/interfaces/adminDB.ts
|
|
24
|
+
var init_adminDB = __esm({
|
|
25
|
+
"src/core/interfaces/adminDB.ts"() {
|
|
26
|
+
"use strict";
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// src/core/interfaces/classroomDB.ts
|
|
31
|
+
var init_classroomDB = __esm({
|
|
32
|
+
"src/core/interfaces/classroomDB.ts"() {
|
|
33
|
+
"use strict";
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
23
37
|
// src/util/logger.ts
|
|
24
38
|
var isDevelopment, logger;
|
|
25
39
|
var init_logger = __esm({
|
|
@@ -65,31 +79,6 @@ var init_logger = __esm({
|
|
|
65
79
|
}
|
|
66
80
|
});
|
|
67
81
|
|
|
68
|
-
// src/core/types/types-legacy.ts
|
|
69
|
-
var GuestUsername, log, cardHistoryPrefix;
|
|
70
|
-
var init_types_legacy = __esm({
|
|
71
|
-
"src/core/types/types-legacy.ts"() {
|
|
72
|
-
"use strict";
|
|
73
|
-
init_logger();
|
|
74
|
-
GuestUsername = "Guest";
|
|
75
|
-
log = (message) => {
|
|
76
|
-
logger.log(message);
|
|
77
|
-
};
|
|
78
|
-
cardHistoryPrefix = "cardH";
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// src/core/util/index.ts
|
|
83
|
-
function getCardHistoryID(courseID, cardID) {
|
|
84
|
-
return `${cardHistoryPrefix}-${courseID}-${cardID}`;
|
|
85
|
-
}
|
|
86
|
-
var init_util = __esm({
|
|
87
|
-
"src/core/util/index.ts"() {
|
|
88
|
-
"use strict";
|
|
89
|
-
init_types_legacy();
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
82
|
// src/impl/couch/pouchdb-setup.ts
|
|
94
83
|
import PouchDB from "pouchdb";
|
|
95
84
|
import PouchDBFind from "pouchdb-find";
|
|
@@ -109,122 +98,6 @@ var init_pouchdb_setup = __esm({
|
|
|
109
98
|
}
|
|
110
99
|
});
|
|
111
100
|
|
|
112
|
-
// src/util/tuiLogger.ts
|
|
113
|
-
var init_tuiLogger = __esm({
|
|
114
|
-
"src/util/tuiLogger.ts"() {
|
|
115
|
-
"use strict";
|
|
116
|
-
init_dataDirectory();
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// src/util/dataDirectory.ts
|
|
121
|
-
import * as path from "path";
|
|
122
|
-
import * as os from "os";
|
|
123
|
-
function getAppDataDirectory() {
|
|
124
|
-
return path.join(os.homedir(), ".tuilder");
|
|
125
|
-
}
|
|
126
|
-
function getDbPath(dbName) {
|
|
127
|
-
return path.join(getAppDataDirectory(), dbName);
|
|
128
|
-
}
|
|
129
|
-
var init_dataDirectory = __esm({
|
|
130
|
-
"src/util/dataDirectory.ts"() {
|
|
131
|
-
"use strict";
|
|
132
|
-
init_tuiLogger();
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// src/impl/common/userDBHelpers.ts
|
|
137
|
-
import moment from "moment";
|
|
138
|
-
function hexEncode(str) {
|
|
139
|
-
let hex;
|
|
140
|
-
let returnStr = "";
|
|
141
|
-
for (let i = 0; i < str.length; i++) {
|
|
142
|
-
hex = str.charCodeAt(i).toString(16);
|
|
143
|
-
returnStr += ("000" + hex).slice(3);
|
|
144
|
-
}
|
|
145
|
-
return returnStr;
|
|
146
|
-
}
|
|
147
|
-
function filterAllDocsByPrefix(db, prefix, opts) {
|
|
148
|
-
const options = {
|
|
149
|
-
startkey: prefix,
|
|
150
|
-
endkey: prefix + "\uFFF0",
|
|
151
|
-
include_docs: true
|
|
152
|
-
};
|
|
153
|
-
if (opts) {
|
|
154
|
-
Object.assign(options, opts);
|
|
155
|
-
}
|
|
156
|
-
return db.allDocs(options);
|
|
157
|
-
}
|
|
158
|
-
function getStartAndEndKeys(key) {
|
|
159
|
-
return {
|
|
160
|
-
startkey: key,
|
|
161
|
-
endkey: key + "\uFFF0"
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function updateGuestAccountExpirationDate(guestDB) {
|
|
165
|
-
const currentTime = moment.utc();
|
|
166
|
-
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
167
|
-
const expiryDocID2 = "GuestAccountExpirationDate";
|
|
168
|
-
void guestDB.get(expiryDocID2).then((doc) => {
|
|
169
|
-
return guestDB.put({
|
|
170
|
-
_id: expiryDocID2,
|
|
171
|
-
_rev: doc._rev,
|
|
172
|
-
date: expirationDate
|
|
173
|
-
});
|
|
174
|
-
}).catch(() => {
|
|
175
|
-
return guestDB.put({
|
|
176
|
-
_id: expiryDocID2,
|
|
177
|
-
date: expirationDate
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
function getLocalUserDB(username) {
|
|
182
|
-
const dbName = `userdb-${username}`;
|
|
183
|
-
if (typeof window === "undefined") {
|
|
184
|
-
return new pouchdb_setup_default(getDbPath(dbName), {});
|
|
185
|
-
} else {
|
|
186
|
-
return new pouchdb_setup_default(dbName, {});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
function scheduleCardReviewLocal(userDB, review) {
|
|
190
|
-
const now = moment.utc();
|
|
191
|
-
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
192
|
-
void userDB.put({
|
|
193
|
-
_id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
|
|
194
|
-
cardId: review.card_id,
|
|
195
|
-
reviewTime: review.time,
|
|
196
|
-
courseId: review.course_id,
|
|
197
|
-
scheduledAt: now,
|
|
198
|
-
scheduledFor: review.scheduledFor,
|
|
199
|
-
schedulingAgentId: review.schedulingAgentId
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
|
|
203
|
-
const reviewDoc = await userDB.get(reviewDocID);
|
|
204
|
-
userDB.remove(reviewDoc).then((res) => {
|
|
205
|
-
if (res.ok) {
|
|
206
|
-
log2(`Removed Review Doc: ${reviewDocID}`);
|
|
207
|
-
}
|
|
208
|
-
}).catch((err) => {
|
|
209
|
-
log2(`Failed to remove Review Doc: ${reviewDocID},
|
|
210
|
-
${JSON.stringify(err)}`);
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
var REVIEW_PREFIX, REVIEW_TIME_FORMAT, log2;
|
|
214
|
-
var init_userDBHelpers = __esm({
|
|
215
|
-
"src/impl/common/userDBHelpers.ts"() {
|
|
216
|
-
"use strict";
|
|
217
|
-
init_logger();
|
|
218
|
-
init_pouchdb_setup();
|
|
219
|
-
init_dataDirectory();
|
|
220
|
-
REVIEW_PREFIX = "card_review_";
|
|
221
|
-
REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
222
|
-
log2 = (s) => {
|
|
223
|
-
logger.info(s);
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
101
|
// src/util/Loggable.ts
|
|
229
102
|
var Loggable;
|
|
230
103
|
var init_Loggable = __esm({
|
|
@@ -252,7 +125,10 @@ var init_updateQueue = __esm({
|
|
|
252
125
|
_className = "UpdateQueue";
|
|
253
126
|
pendingUpdates = {};
|
|
254
127
|
inprogressUpdates = {};
|
|
255
|
-
|
|
128
|
+
readDB;
|
|
129
|
+
// Database for read operations
|
|
130
|
+
writeDB;
|
|
131
|
+
// Database for write operations (local-first)
|
|
256
132
|
update(id, update) {
|
|
257
133
|
logger.debug(`Update requested on doc: ${id}`);
|
|
258
134
|
if (this.pendingUpdates[id]) {
|
|
@@ -262,24 +138,25 @@ var init_updateQueue = __esm({
|
|
|
262
138
|
}
|
|
263
139
|
return this.applyUpdates(id);
|
|
264
140
|
}
|
|
265
|
-
constructor(
|
|
141
|
+
constructor(readDB, writeDB) {
|
|
266
142
|
super();
|
|
267
|
-
this.
|
|
143
|
+
this.readDB = readDB;
|
|
144
|
+
this.writeDB = writeDB || readDB;
|
|
268
145
|
logger.debug(`UpdateQ initialized...`);
|
|
269
|
-
void this.
|
|
146
|
+
void this.readDB.info().then((i) => {
|
|
270
147
|
logger.debug(`db info: ${JSON.stringify(i)}`);
|
|
271
148
|
});
|
|
272
149
|
}
|
|
273
150
|
async applyUpdates(id) {
|
|
274
151
|
logger.debug(`Applying updates on doc: ${id}`);
|
|
275
152
|
if (this.inprogressUpdates[id]) {
|
|
276
|
-
await this.
|
|
153
|
+
await this.readDB.info();
|
|
277
154
|
return this.applyUpdates(id);
|
|
278
155
|
} else {
|
|
279
156
|
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
280
157
|
this.inprogressUpdates[id] = true;
|
|
281
158
|
try {
|
|
282
|
-
let doc = await this.
|
|
159
|
+
let doc = await this.readDB.get(id);
|
|
283
160
|
logger.debug(`Retrieved doc: ${id}`);
|
|
284
161
|
while (this.pendingUpdates[id].length !== 0) {
|
|
285
162
|
const update = this.pendingUpdates[id].splice(0, 1)[0];
|
|
@@ -292,7 +169,7 @@ var init_updateQueue = __esm({
|
|
|
292
169
|
};
|
|
293
170
|
}
|
|
294
171
|
}
|
|
295
|
-
await this.
|
|
172
|
+
await this.writeDB.put(doc);
|
|
296
173
|
logger.debug(`Put doc: ${id}`);
|
|
297
174
|
if (this.pendingUpdates[id].length === 0) {
|
|
298
175
|
this.inprogressUpdates[id] = false;
|
|
@@ -318,6 +195,32 @@ var init_updateQueue = __esm({
|
|
|
318
195
|
}
|
|
319
196
|
});
|
|
320
197
|
|
|
198
|
+
// src/core/types/types-legacy.ts
|
|
199
|
+
var GuestUsername, log, DocTypePrefixes;
|
|
200
|
+
var init_types_legacy = __esm({
|
|
201
|
+
"src/core/types/types-legacy.ts"() {
|
|
202
|
+
"use strict";
|
|
203
|
+
init_logger();
|
|
204
|
+
GuestUsername = "Guest";
|
|
205
|
+
log = (message) => {
|
|
206
|
+
logger.log(message);
|
|
207
|
+
};
|
|
208
|
+
DocTypePrefixes = {
|
|
209
|
+
["CARD" /* CARD */]: "c",
|
|
210
|
+
["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]: "dd",
|
|
211
|
+
["TAG" /* TAG */]: "TAG",
|
|
212
|
+
["CARDRECORD" /* CARDRECORD */]: "cardH",
|
|
213
|
+
["SCHEDULED_CARD" /* SCHEDULED_CARD */]: "card_review_",
|
|
214
|
+
// Add other doctypes here as they get prefixed IDs
|
|
215
|
+
["DATASHAPE" /* DATASHAPE */]: "DATASHAPE",
|
|
216
|
+
["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
|
|
217
|
+
["VIEW" /* VIEW */]: "VIEW",
|
|
218
|
+
["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
|
|
219
|
+
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
321
224
|
// src/impl/couch/clientCache.ts
|
|
322
225
|
async function GET_CACHED(k, f) {
|
|
323
226
|
if (CLIENT_CACHE[k]) {
|
|
@@ -341,10 +244,12 @@ var init_clientCache = __esm({
|
|
|
341
244
|
import { NameSpacer } from "@vue-skuilder/common";
|
|
342
245
|
import { blankCourseElo, toCourseElo } from "@vue-skuilder/common";
|
|
343
246
|
import { prepareNote55 } from "@vue-skuilder/common";
|
|
247
|
+
import { v4 as uuidv4 } from "uuid";
|
|
344
248
|
async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = blankCourseElo()) {
|
|
345
249
|
const db = getCourseDB(courseID);
|
|
346
250
|
const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads);
|
|
347
|
-
const
|
|
251
|
+
const _id = `${DocTypePrefixes["DISPLAYABLE_DATA" /* DISPLAYABLE_DATA */]}-${uuidv4()}`;
|
|
252
|
+
const result = await db.put({ ...payload, _id });
|
|
348
253
|
const dataShapeId = NameSpacer.getDataShapeString({
|
|
349
254
|
course: codeCourse,
|
|
350
255
|
dataShape: shape.name
|
|
@@ -415,7 +320,9 @@ async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags
|
|
|
415
320
|
}
|
|
416
321
|
async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
|
|
417
322
|
const db = getCourseDB(courseID);
|
|
418
|
-
const
|
|
323
|
+
const _id = `${DocTypePrefixes["CARD" /* CARD */]}-${uuidv4()}`;
|
|
324
|
+
const card = await db.put({
|
|
325
|
+
_id,
|
|
419
326
|
course,
|
|
420
327
|
id_displayable_data,
|
|
421
328
|
id_view,
|
|
@@ -794,7 +701,7 @@ async function getCourseQuestionTypes(courseID) {
|
|
|
794
701
|
}
|
|
795
702
|
async function getCourseTagStubs(courseID) {
|
|
796
703
|
logger.debug(`Getting tag stubs for course: ${courseID}`);
|
|
797
|
-
const stubs = await
|
|
704
|
+
const stubs = await filterAllDocsByPrefix(
|
|
798
705
|
getCourseDB2(courseID),
|
|
799
706
|
"TAG" /* TAG */.valueOf() + "-"
|
|
800
707
|
);
|
|
@@ -859,7 +766,7 @@ function getAncestorTagIDs(courseID, tagID) {
|
|
|
859
766
|
}
|
|
860
767
|
}
|
|
861
768
|
async function getChildTagStubs(courseID, tagID) {
|
|
862
|
-
return await
|
|
769
|
+
return await filterAllDocsByPrefix(getCourseDB2(courseID), tagID + ">");
|
|
863
770
|
}
|
|
864
771
|
async function getAppliedTags(id_course, id_card) {
|
|
865
772
|
const db = getCourseDB2(id_course);
|
|
@@ -1365,1217 +1272,1418 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
1365
1272
|
}
|
|
1366
1273
|
});
|
|
1367
1274
|
|
|
1368
|
-
// src/impl/couch/
|
|
1369
|
-
import
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1275
|
+
// src/impl/couch/classroomDB.ts
|
|
1276
|
+
import moment from "moment";
|
|
1277
|
+
function getClassroomDB(classID, version) {
|
|
1278
|
+
const dbName = `classdb-${version}-${classID}`;
|
|
1279
|
+
logger.info(`Retrieving classroom db: ${dbName}`);
|
|
1280
|
+
return new pouchdb_setup_default(
|
|
1281
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1282
|
+
pouchDBincludeCredentialsConfig
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
async function getClassroomConfig(classID) {
|
|
1286
|
+
return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
|
|
1287
|
+
}
|
|
1288
|
+
var classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
|
|
1289
|
+
var init_classroomDB2 = __esm({
|
|
1290
|
+
"src/impl/couch/classroomDB.ts"() {
|
|
1373
1291
|
"use strict";
|
|
1292
|
+
init_factory();
|
|
1293
|
+
init_logger();
|
|
1294
|
+
init_pouchdb_setup();
|
|
1374
1295
|
init_couch();
|
|
1375
1296
|
init_courseDB();
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
async getReviewsForcast(daysCount) {
|
|
1387
|
-
const time = moment2.utc().add(daysCount, "days");
|
|
1388
|
-
return this.getReviewstoDate(time);
|
|
1389
|
-
}
|
|
1390
|
-
async getPendingReviews() {
|
|
1391
|
-
const now = moment2.utc();
|
|
1392
|
-
return this.getReviewstoDate(now);
|
|
1297
|
+
classroomLookupDBTitle = "classdb-lookup";
|
|
1298
|
+
CLASSROOM_CONFIG = "ClassroomConfig";
|
|
1299
|
+
ClassroomDBBase = class {
|
|
1300
|
+
_id;
|
|
1301
|
+
_db;
|
|
1302
|
+
_cfg;
|
|
1303
|
+
_initComplete = false;
|
|
1304
|
+
_content_prefix = "content";
|
|
1305
|
+
get _content_searchkeys() {
|
|
1306
|
+
return getStartAndEndKeys(this._content_prefix);
|
|
1393
1307
|
}
|
|
1394
|
-
async
|
|
1395
|
-
|
|
1308
|
+
async getAssignedContent() {
|
|
1309
|
+
logger.info(`Getting assigned content...`);
|
|
1310
|
+
const docRows = await this._db.allDocs({
|
|
1311
|
+
startkey: this._content_prefix,
|
|
1312
|
+
endkey: this._content_prefix + `\uFFF0`,
|
|
1313
|
+
include_docs: true
|
|
1314
|
+
});
|
|
1315
|
+
const ret = docRows.rows.map((row) => {
|
|
1316
|
+
return row.doc;
|
|
1317
|
+
});
|
|
1318
|
+
return ret;
|
|
1396
1319
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
if (crsDoc && crsDoc.settings) {
|
|
1401
|
-
return crsDoc.settings;
|
|
1320
|
+
getContentId(content) {
|
|
1321
|
+
if (content.type === "tag") {
|
|
1322
|
+
return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
|
|
1402
1323
|
} else {
|
|
1403
|
-
|
|
1404
|
-
return {};
|
|
1324
|
+
return `${this._content_prefix}-${content.courseID}`;
|
|
1405
1325
|
}
|
|
1406
1326
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1327
|
+
get ready() {
|
|
1328
|
+
return this._initComplete;
|
|
1409
1329
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const reviews = await this.user.remote().allDocs({
|
|
1413
|
-
startkey: keys.startkey,
|
|
1414
|
-
endkey: keys.endkey,
|
|
1415
|
-
include_docs: true
|
|
1416
|
-
});
|
|
1417
|
-
logger.debug(
|
|
1418
|
-
`Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
|
|
1419
|
-
);
|
|
1420
|
-
return reviews.rows.filter((r) => {
|
|
1421
|
-
if (r.id.startsWith(REVIEW_PREFIX2)) {
|
|
1422
|
-
const date = moment2.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
|
|
1423
|
-
if (targetDate.isAfter(date)) {
|
|
1424
|
-
if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
|
|
1425
|
-
return true;
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
}).map((r) => r.doc);
|
|
1330
|
+
getConfig() {
|
|
1331
|
+
return this._cfg;
|
|
1430
1332
|
}
|
|
1431
1333
|
};
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
try {
|
|
1441
|
-
ret = await getLocalUserDB(user).get(userClassroomsDoc);
|
|
1442
|
-
} catch (e) {
|
|
1443
|
-
const err = e;
|
|
1444
|
-
if (err.status === 404) {
|
|
1445
|
-
await getLocalUserDB(user).put({
|
|
1446
|
-
_id: userClassroomsDoc,
|
|
1447
|
-
registrations: []
|
|
1448
|
-
});
|
|
1449
|
-
ret = await getOrCreateClassroomRegistrationsDoc(user);
|
|
1450
|
-
} else {
|
|
1451
|
-
const errorDetails = {
|
|
1452
|
-
name: err.name,
|
|
1453
|
-
status: err.status,
|
|
1454
|
-
message: err.message,
|
|
1455
|
-
reason: err.reason,
|
|
1456
|
-
error: err.error
|
|
1457
|
-
};
|
|
1458
|
-
logger.error(
|
|
1459
|
-
"Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
|
|
1460
|
-
errorDetails
|
|
1461
|
-
);
|
|
1462
|
-
throw new Error(
|
|
1463
|
-
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
1464
|
-
);
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
return ret;
|
|
1468
|
-
}
|
|
1469
|
-
async function getOrCreateCourseRegistrationsDoc(user) {
|
|
1470
|
-
let ret;
|
|
1471
|
-
try {
|
|
1472
|
-
ret = await getLocalUserDB(user).get(userCoursesDoc);
|
|
1473
|
-
} catch (e) {
|
|
1474
|
-
const err = e;
|
|
1475
|
-
if (err.status === 404) {
|
|
1476
|
-
await getLocalUserDB(user).put({
|
|
1477
|
-
_id: userCoursesDoc,
|
|
1478
|
-
courses: [],
|
|
1479
|
-
studyWeight: {}
|
|
1480
|
-
});
|
|
1481
|
-
ret = await getOrCreateCourseRegistrationsDoc(user);
|
|
1482
|
-
} else {
|
|
1483
|
-
throw new Error(
|
|
1484
|
-
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
1485
|
-
);
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
return ret;
|
|
1489
|
-
}
|
|
1490
|
-
async function updateUserElo(user, course_id, elo) {
|
|
1491
|
-
const regDoc = await getOrCreateCourseRegistrationsDoc(user);
|
|
1492
|
-
const course = regDoc.courses.find((c) => c.courseID === course_id);
|
|
1493
|
-
course.elo = elo;
|
|
1494
|
-
return getLocalUserDB(user).put(regDoc);
|
|
1495
|
-
}
|
|
1496
|
-
async function registerUserForClassroom(user, classID, registerAs) {
|
|
1497
|
-
log3(`Registering user: ${user} in course: ${classID}`);
|
|
1498
|
-
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
1499
|
-
const regItem = {
|
|
1500
|
-
classID,
|
|
1501
|
-
registeredAs: registerAs
|
|
1502
|
-
};
|
|
1503
|
-
if (doc.registrations.filter((reg) => {
|
|
1504
|
-
return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
|
|
1505
|
-
}).length === 0) {
|
|
1506
|
-
doc.registrations.push(regItem);
|
|
1507
|
-
} else {
|
|
1508
|
-
log3(`User ${user} is already registered for class ${classID}`);
|
|
1509
|
-
}
|
|
1510
|
-
return getLocalUserDB(user).put(doc);
|
|
1511
|
-
});
|
|
1512
|
-
}
|
|
1513
|
-
async function dropUserFromClassroom(user, classID) {
|
|
1514
|
-
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
1515
|
-
let index = -1;
|
|
1516
|
-
for (let i = 0; i < doc.registrations.length; i++) {
|
|
1517
|
-
if (doc.registrations[i].classID === classID) {
|
|
1518
|
-
index = i;
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
if (index !== -1) {
|
|
1522
|
-
doc.registrations.splice(index, 1);
|
|
1523
|
-
}
|
|
1524
|
-
return getLocalUserDB(user).put(doc);
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
async function getUserClassrooms(user) {
|
|
1528
|
-
return getOrCreateClassroomRegistrationsDoc(user);
|
|
1529
|
-
}
|
|
1530
|
-
var log3, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
1531
|
-
var init_BaseUserDB = __esm({
|
|
1532
|
-
"src/impl/common/BaseUserDB.ts"() {
|
|
1533
|
-
"use strict";
|
|
1534
|
-
init_util();
|
|
1535
|
-
init_types_legacy();
|
|
1536
|
-
init_logger();
|
|
1537
|
-
init_userDBHelpers();
|
|
1538
|
-
init_updateQueue();
|
|
1539
|
-
init_user_course_relDB();
|
|
1540
|
-
init_couch();
|
|
1541
|
-
log3 = (s) => {
|
|
1542
|
-
logger.info(s);
|
|
1543
|
-
};
|
|
1544
|
-
cardHistoryPrefix2 = "cardH-";
|
|
1545
|
-
BaseUser = class _BaseUser {
|
|
1546
|
-
static _instance;
|
|
1547
|
-
static _initialized = false;
|
|
1548
|
-
static Dummy(syncStrategy) {
|
|
1549
|
-
return new _BaseUser("Me", syncStrategy);
|
|
1334
|
+
StudentClassroomDB = class _StudentClassroomDB extends ClassroomDBBase {
|
|
1335
|
+
// private readonly _prefix: string = 'content';
|
|
1336
|
+
userMessages;
|
|
1337
|
+
_user;
|
|
1338
|
+
constructor(classID, user) {
|
|
1339
|
+
super();
|
|
1340
|
+
this._id = classID;
|
|
1341
|
+
this._user = user;
|
|
1550
1342
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1343
|
+
async init() {
|
|
1344
|
+
const dbName = `classdb-student-${this._id}`;
|
|
1345
|
+
this._db = new pouchdb_setup_default(
|
|
1346
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1347
|
+
pouchDBincludeCredentialsConfig
|
|
1348
|
+
);
|
|
1349
|
+
try {
|
|
1350
|
+
const cfg = await this._db.get(CLASSROOM_CONFIG);
|
|
1351
|
+
this._cfg = cfg;
|
|
1352
|
+
this.userMessages = this._db.changes({
|
|
1353
|
+
since: "now",
|
|
1354
|
+
live: true,
|
|
1355
|
+
include_docs: true
|
|
1356
|
+
});
|
|
1357
|
+
this._initComplete = true;
|
|
1358
|
+
return;
|
|
1359
|
+
} catch (e) {
|
|
1360
|
+
throw new Error(`Error in StudentClassroomDB constructor: ${JSON.stringify(e)}`);
|
|
1361
|
+
}
|
|
1561
1362
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1363
|
+
static async factory(classID, user) {
|
|
1364
|
+
const ret = new _StudentClassroomDB(classID, user);
|
|
1365
|
+
await ret.init();
|
|
1366
|
+
return ret;
|
|
1564
1367
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
return this.remoteDB;
|
|
1368
|
+
setChangeFcn(f) {
|
|
1369
|
+
void this.userMessages.on("change", f);
|
|
1568
1370
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
log3(`Account created successfully, updating username to ${username}`);
|
|
1584
|
-
this._username = username;
|
|
1585
|
-
try {
|
|
1586
|
-
localStorage.removeItem("dbUUID");
|
|
1587
|
-
} catch (e) {
|
|
1588
|
-
logger.warn("localStorage not available (Node.js environment):", e);
|
|
1589
|
-
}
|
|
1590
|
-
await this.init();
|
|
1591
|
-
}
|
|
1592
|
-
return {
|
|
1593
|
-
status: result.status,
|
|
1594
|
-
error: result.error || ""
|
|
1595
|
-
};
|
|
1371
|
+
async getPendingReviews() {
|
|
1372
|
+
const u = this._user;
|
|
1373
|
+
return (await u.getPendingReviews()).filter((r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id).map((r) => {
|
|
1374
|
+
return {
|
|
1375
|
+
...r,
|
|
1376
|
+
qualifiedID: `${r.courseId}-${r.cardId}`,
|
|
1377
|
+
courseID: r.courseId,
|
|
1378
|
+
cardID: r.cardId,
|
|
1379
|
+
contentSourceType: "classroom",
|
|
1380
|
+
contentSourceID: this._id,
|
|
1381
|
+
reviewID: r._id,
|
|
1382
|
+
status: "review"
|
|
1383
|
+
};
|
|
1384
|
+
});
|
|
1596
1385
|
}
|
|
1597
|
-
async
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1386
|
+
async getNewCards() {
|
|
1387
|
+
const activeCards = await this._user.getActiveCards();
|
|
1388
|
+
const now = moment.utc();
|
|
1389
|
+
const assigned = await this.getAssignedContent();
|
|
1390
|
+
const due = assigned.filter((c) => now.isAfter(moment.utc(c.activeOn, REVIEW_TIME_FORMAT)));
|
|
1391
|
+
logger.info(`Due content: ${JSON.stringify(due)}`);
|
|
1392
|
+
let ret = [];
|
|
1393
|
+
for (let i = 0; i < due.length; i++) {
|
|
1394
|
+
const content = due[i];
|
|
1395
|
+
if (content.type === "course") {
|
|
1396
|
+
const db = new CourseDB(content.courseID, async () => this._user);
|
|
1397
|
+
ret = ret.concat(await db.getNewCards());
|
|
1398
|
+
} else if (content.type === "tag") {
|
|
1399
|
+
const tagDoc = await getTag(content.courseID, content.tagID);
|
|
1400
|
+
ret = ret.concat(
|
|
1401
|
+
tagDoc.taggedCards.map((c) => {
|
|
1402
|
+
return {
|
|
1403
|
+
courseID: content.courseID,
|
|
1404
|
+
cardID: c,
|
|
1405
|
+
qualifiedID: `${content.courseID}-${c}`,
|
|
1406
|
+
contentSourceType: "classroom",
|
|
1407
|
+
contentSourceID: this._id,
|
|
1408
|
+
status: "new"
|
|
1409
|
+
};
|
|
1410
|
+
})
|
|
1411
|
+
);
|
|
1412
|
+
} else if (content.type === "card") {
|
|
1413
|
+
ret.push(await getCourseDB2(content.courseID).get(content.cardID));
|
|
1605
1414
|
}
|
|
1606
|
-
logger.warn(`User ${this._username} is already logged in, but executing login again.`);
|
|
1607
1415
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
} catch (e) {
|
|
1615
|
-
logger.warn("localStorage not available (Node.js environment):", e);
|
|
1416
|
+
logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
|
|
1417
|
+
return ret.filter((c) => {
|
|
1418
|
+
if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
|
|
1419
|
+
return false;
|
|
1420
|
+
} else {
|
|
1421
|
+
return true;
|
|
1616
1422
|
}
|
|
1617
|
-
|
|
1618
|
-
}
|
|
1619
|
-
return loginResult;
|
|
1423
|
+
});
|
|
1620
1424
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
}
|
|
1628
|
-
try {
|
|
1629
|
-
const localDB = getLocalUserDB(this._username);
|
|
1630
|
-
const allDocs = await localDB.allDocs({ include_docs: false });
|
|
1631
|
-
const docsToDelete = allDocs.rows.filter((row) => {
|
|
1632
|
-
const id = row.id;
|
|
1633
|
-
return id.startsWith(cardHistoryPrefix2) || // Card interaction history
|
|
1634
|
-
id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
|
|
1635
|
-
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
1636
|
-
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
1637
|
-
id === _BaseUser.DOC_IDS.CONFIG;
|
|
1638
|
-
}).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
|
|
1639
|
-
if (docsToDelete.length > 0) {
|
|
1640
|
-
await localDB.bulkDocs(docsToDelete);
|
|
1641
|
-
}
|
|
1642
|
-
await this.init();
|
|
1643
|
-
return { status: Status2.ok };
|
|
1644
|
-
} catch (error) {
|
|
1645
|
-
logger.error("Failed to reset user data:", error);
|
|
1646
|
-
return {
|
|
1647
|
-
status: Status2.error,
|
|
1648
|
-
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
async logout() {
|
|
1653
|
-
if (!this.syncStrategy.canAuthenticate()) {
|
|
1654
|
-
this._username = await this.syncStrategy.getCurrentUsername();
|
|
1655
|
-
await this.init();
|
|
1656
|
-
return { ok: true };
|
|
1657
|
-
}
|
|
1658
|
-
const ret = await this.syncStrategy.logout();
|
|
1659
|
-
this._username = await this.syncStrategy.getCurrentUsername();
|
|
1660
|
-
await this.init();
|
|
1661
|
-
return ret;
|
|
1662
|
-
}
|
|
1663
|
-
update(id, update) {
|
|
1664
|
-
return this.updateQueue.update(id, update);
|
|
1425
|
+
};
|
|
1426
|
+
TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
|
|
1427
|
+
_stuDb;
|
|
1428
|
+
constructor(classID) {
|
|
1429
|
+
super();
|
|
1430
|
+
this._id = classID;
|
|
1665
1431
|
}
|
|
1666
|
-
async
|
|
1667
|
-
|
|
1668
|
-
|
|
1432
|
+
async init() {
|
|
1433
|
+
const dbName = `classdb-teacher-${this._id}`;
|
|
1434
|
+
const stuDbName = `classdb-student-${this._id}`;
|
|
1435
|
+
this._db = new pouchdb_setup_default(
|
|
1436
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1437
|
+
pouchDBincludeCredentialsConfig
|
|
1438
|
+
);
|
|
1439
|
+
this._stuDb = new pouchdb_setup_default(
|
|
1440
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
|
|
1441
|
+
pouchDBincludeCredentialsConfig
|
|
1442
|
+
);
|
|
1669
1443
|
try {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1444
|
+
return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
|
|
1445
|
+
this._cfg = cfg;
|
|
1446
|
+
this._initComplete = true;
|
|
1447
|
+
}).then(() => {
|
|
1448
|
+
return;
|
|
1449
|
+
});
|
|
1674
1450
|
} catch (e) {
|
|
1675
|
-
|
|
1676
|
-
if (err.status === 404) {
|
|
1677
|
-
await this.localDB.put({
|
|
1678
|
-
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
1679
|
-
courses: [],
|
|
1680
|
-
studyWeight: {}
|
|
1681
|
-
});
|
|
1682
|
-
ret = await this.getCourseRegistrationsDoc();
|
|
1683
|
-
} else {
|
|
1684
|
-
throw new Error(
|
|
1685
|
-
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
1686
|
-
);
|
|
1687
|
-
}
|
|
1451
|
+
throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
|
|
1688
1452
|
}
|
|
1689
|
-
return ret;
|
|
1690
1453
|
}
|
|
1691
|
-
async
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
/**
|
|
1698
|
-
* Returns a promise of the card IDs that the user has
|
|
1699
|
-
* a scheduled review for.
|
|
1700
|
-
*
|
|
1701
|
-
*/
|
|
1702
|
-
async getActiveCards() {
|
|
1703
|
-
const keys = getStartAndEndKeys(REVIEW_PREFIX);
|
|
1704
|
-
const reviews = await this.remoteDB.allDocs({
|
|
1705
|
-
startkey: keys.startkey,
|
|
1706
|
-
endkey: keys.endkey,
|
|
1707
|
-
include_docs: true
|
|
1708
|
-
});
|
|
1709
|
-
return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
|
|
1454
|
+
static async factory(classID) {
|
|
1455
|
+
const ret = new _TeacherClassroomDB(classID);
|
|
1456
|
+
await ret.init();
|
|
1457
|
+
return ret;
|
|
1710
1458
|
}
|
|
1711
|
-
async
|
|
1459
|
+
async removeContent(content) {
|
|
1460
|
+
const contentID = this.getContentId(content);
|
|
1712
1461
|
try {
|
|
1713
|
-
const
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
for (let i = 0; i < hist.length; i++) {
|
|
1721
|
-
try {
|
|
1722
|
-
if (hist[i] && Array.isArray(hist[i].records)) {
|
|
1723
|
-
hist[i].records.forEach((record) => {
|
|
1724
|
-
try {
|
|
1725
|
-
if (!record.timeStamp) {
|
|
1726
|
-
return;
|
|
1727
|
-
}
|
|
1728
|
-
let timeStamp;
|
|
1729
|
-
if (typeof record.timeStamp === "object") {
|
|
1730
|
-
if (typeof record.timeStamp.toDate === "function") {
|
|
1731
|
-
timeStamp = record.timeStamp.toISOString();
|
|
1732
|
-
} else if (record.timeStamp instanceof Date) {
|
|
1733
|
-
timeStamp = record.timeStamp.toISOString();
|
|
1734
|
-
} else {
|
|
1735
|
-
if (sampleCount < 3) {
|
|
1736
|
-
logger.warn("Unknown timestamp object type:", record.timeStamp);
|
|
1737
|
-
sampleCount++;
|
|
1738
|
-
}
|
|
1739
|
-
return;
|
|
1740
|
-
}
|
|
1741
|
-
} else if (typeof record.timeStamp === "string") {
|
|
1742
|
-
const date = new Date(record.timeStamp);
|
|
1743
|
-
if (isNaN(date.getTime())) {
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
timeStamp = record.timeStamp;
|
|
1747
|
-
} else if (typeof record.timeStamp === "number") {
|
|
1748
|
-
timeStamp = new Date(record.timeStamp).toISOString();
|
|
1749
|
-
} else {
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
allRecords.push({
|
|
1753
|
-
timeStamp,
|
|
1754
|
-
courseID: record.courseID || "unknown",
|
|
1755
|
-
cardID: record.cardID || "unknown",
|
|
1756
|
-
timeSpent: record.timeSpent || 0,
|
|
1757
|
-
type: "card_view"
|
|
1758
|
-
});
|
|
1759
|
-
} catch (err) {
|
|
1760
|
-
}
|
|
1761
|
-
});
|
|
1762
|
-
}
|
|
1763
|
-
} catch (err) {
|
|
1764
|
-
logger.error("Error processing history item:", err);
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
logger.debug(`Found ${allRecords.length} activity records`);
|
|
1768
|
-
return allRecords;
|
|
1769
|
-
} catch (err) {
|
|
1770
|
-
logger.error("Error in getActivityRecords:", err);
|
|
1771
|
-
return [];
|
|
1462
|
+
const doc = await this._db.get(contentID);
|
|
1463
|
+
await this._db.remove(doc);
|
|
1464
|
+
void this._db.replicate.to(this._stuDb, {
|
|
1465
|
+
doc_ids: [contentID]
|
|
1466
|
+
});
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
logger.error("Failed to remove content:", contentID, error);
|
|
1772
1469
|
}
|
|
1773
1470
|
}
|
|
1774
|
-
async
|
|
1775
|
-
|
|
1776
|
-
const
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1471
|
+
async assignContent(content) {
|
|
1472
|
+
let put;
|
|
1473
|
+
const id = this.getContentId(content);
|
|
1474
|
+
if (content.type === "tag") {
|
|
1475
|
+
put = await this._db.put({
|
|
1476
|
+
courseID: content.courseID,
|
|
1477
|
+
tagID: content.tagID,
|
|
1478
|
+
type: "tag",
|
|
1479
|
+
_id: id,
|
|
1480
|
+
assignedBy: content.assignedBy,
|
|
1481
|
+
assignedOn: moment.utc(),
|
|
1482
|
+
activeOn: content.activeOn || moment.utc()
|
|
1483
|
+
});
|
|
1484
|
+
} else {
|
|
1485
|
+
put = await this._db.put({
|
|
1486
|
+
courseID: content.courseID,
|
|
1487
|
+
type: "course",
|
|
1488
|
+
_id: id,
|
|
1489
|
+
assignedBy: content.assignedBy,
|
|
1490
|
+
assignedOn: moment.utc(),
|
|
1491
|
+
activeOn: content.activeOn || moment.utc()
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (put.ok) {
|
|
1495
|
+
void this._db.replicate.to(this._stuDb, {
|
|
1496
|
+
doc_ids: [id]
|
|
1497
|
+
});
|
|
1498
|
+
return true;
|
|
1499
|
+
} else {
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
|
|
1505
|
+
skip_setup: true
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
// src/core/interfaces/contentSource.ts
|
|
1511
|
+
function isReview(item) {
|
|
1512
|
+
const ret = item.status === "review" || item.status === "failed-review" || "reviewID" in item;
|
|
1513
|
+
return ret;
|
|
1514
|
+
}
|
|
1515
|
+
async function getStudySource(source, user) {
|
|
1516
|
+
if (source.type === "classroom") {
|
|
1517
|
+
return await StudentClassroomDB.factory(source.id, user);
|
|
1518
|
+
} else {
|
|
1519
|
+
return getDataLayer().getCourseDB(source.id);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
var init_contentSource = __esm({
|
|
1523
|
+
"src/core/interfaces/contentSource.ts"() {
|
|
1524
|
+
"use strict";
|
|
1525
|
+
init_factory();
|
|
1526
|
+
init_classroomDB2();
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
// src/core/interfaces/courseDB.ts
|
|
1531
|
+
var init_courseDB2 = __esm({
|
|
1532
|
+
"src/core/interfaces/courseDB.ts"() {
|
|
1533
|
+
"use strict";
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
// src/core/interfaces/dataLayerProvider.ts
|
|
1538
|
+
var init_dataLayerProvider = __esm({
|
|
1539
|
+
"src/core/interfaces/dataLayerProvider.ts"() {
|
|
1540
|
+
"use strict";
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
// src/core/interfaces/userDB.ts
|
|
1545
|
+
var init_userDB = __esm({
|
|
1546
|
+
"src/core/interfaces/userDB.ts"() {
|
|
1547
|
+
"use strict";
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
// src/core/interfaces/index.ts
|
|
1552
|
+
var init_interfaces = __esm({
|
|
1553
|
+
"src/core/interfaces/index.ts"() {
|
|
1554
|
+
"use strict";
|
|
1555
|
+
init_adminDB();
|
|
1556
|
+
init_classroomDB();
|
|
1557
|
+
init_contentSource();
|
|
1558
|
+
init_courseDB2();
|
|
1559
|
+
init_dataLayerProvider();
|
|
1560
|
+
init_userDB();
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
// src/core/types/user.ts
|
|
1565
|
+
var init_user = __esm({
|
|
1566
|
+
"src/core/types/user.ts"() {
|
|
1567
|
+
"use strict";
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
// src/core/util/index.ts
|
|
1572
|
+
function getCardHistoryID(courseID, cardID) {
|
|
1573
|
+
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
1574
|
+
}
|
|
1575
|
+
var init_util = __esm({
|
|
1576
|
+
"src/core/util/index.ts"() {
|
|
1577
|
+
"use strict";
|
|
1578
|
+
init_types_legacy();
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
// src/core/bulkImport/cardProcessor.ts
|
|
1583
|
+
import { Status as Status2 } from "@vue-skuilder/common";
|
|
1584
|
+
var init_cardProcessor = __esm({
|
|
1585
|
+
"src/core/bulkImport/cardProcessor.ts"() {
|
|
1586
|
+
"use strict";
|
|
1587
|
+
init_logger();
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
// src/core/bulkImport/types.ts
|
|
1592
|
+
var init_types = __esm({
|
|
1593
|
+
"src/core/bulkImport/types.ts"() {
|
|
1594
|
+
"use strict";
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// src/core/bulkImport/index.ts
|
|
1599
|
+
var init_bulkImport = __esm({
|
|
1600
|
+
"src/core/bulkImport/index.ts"() {
|
|
1601
|
+
"use strict";
|
|
1602
|
+
init_cardProcessor();
|
|
1603
|
+
init_types();
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
// src/core/index.ts
|
|
1608
|
+
var init_core = __esm({
|
|
1609
|
+
"src/core/index.ts"() {
|
|
1610
|
+
"use strict";
|
|
1611
|
+
init_interfaces();
|
|
1612
|
+
init_types_legacy();
|
|
1613
|
+
init_user();
|
|
1614
|
+
init_Loggable();
|
|
1615
|
+
init_util();
|
|
1616
|
+
init_navigators();
|
|
1617
|
+
init_bulkImport();
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
// src/util/tuiLogger.ts
|
|
1622
|
+
var init_tuiLogger = __esm({
|
|
1623
|
+
"src/util/tuiLogger.ts"() {
|
|
1624
|
+
"use strict";
|
|
1625
|
+
init_dataDirectory();
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
// src/util/dataDirectory.ts
|
|
1630
|
+
import * as path from "path";
|
|
1631
|
+
import * as os from "os";
|
|
1632
|
+
function getAppDataDirectory() {
|
|
1633
|
+
return path.join(os.homedir(), ".tuilder");
|
|
1634
|
+
}
|
|
1635
|
+
function getDbPath(dbName) {
|
|
1636
|
+
return path.join(getAppDataDirectory(), dbName);
|
|
1637
|
+
}
|
|
1638
|
+
var init_dataDirectory = __esm({
|
|
1639
|
+
"src/util/dataDirectory.ts"() {
|
|
1640
|
+
"use strict";
|
|
1641
|
+
init_tuiLogger();
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// src/impl/common/userDBHelpers.ts
|
|
1646
|
+
import moment2 from "moment";
|
|
1647
|
+
function hexEncode(str) {
|
|
1648
|
+
let hex;
|
|
1649
|
+
let returnStr = "";
|
|
1650
|
+
for (let i = 0; i < str.length; i++) {
|
|
1651
|
+
hex = str.charCodeAt(i).toString(16);
|
|
1652
|
+
returnStr += ("000" + hex).slice(3);
|
|
1653
|
+
}
|
|
1654
|
+
return returnStr;
|
|
1655
|
+
}
|
|
1656
|
+
function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
1657
|
+
const options = {
|
|
1658
|
+
startkey: prefix,
|
|
1659
|
+
endkey: prefix + "\uFFF0",
|
|
1660
|
+
include_docs: true
|
|
1661
|
+
};
|
|
1662
|
+
if (opts) {
|
|
1663
|
+
Object.assign(options, opts);
|
|
1664
|
+
}
|
|
1665
|
+
return db.allDocs(options);
|
|
1666
|
+
}
|
|
1667
|
+
function getStartAndEndKeys2(key) {
|
|
1668
|
+
return {
|
|
1669
|
+
startkey: key,
|
|
1670
|
+
endkey: key + "\uFFF0"
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function updateGuestAccountExpirationDate(guestDB) {
|
|
1674
|
+
const currentTime = moment2.utc();
|
|
1675
|
+
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
1676
|
+
const expiryDocID2 = "GuestAccountExpirationDate";
|
|
1677
|
+
void guestDB.get(expiryDocID2).then((doc) => {
|
|
1678
|
+
return guestDB.put({
|
|
1679
|
+
_id: expiryDocID2,
|
|
1680
|
+
_rev: doc._rev,
|
|
1681
|
+
date: expirationDate
|
|
1682
|
+
});
|
|
1683
|
+
}).catch(() => {
|
|
1684
|
+
return guestDB.put({
|
|
1685
|
+
_id: expiryDocID2,
|
|
1686
|
+
date: expirationDate
|
|
1687
|
+
});
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
function getLocalUserDB(username) {
|
|
1691
|
+
const dbName = `userdb-${username}`;
|
|
1692
|
+
if (typeof window === "undefined") {
|
|
1693
|
+
return new pouchdb_setup_default(getDbPath(dbName), {});
|
|
1694
|
+
} else {
|
|
1695
|
+
return new pouchdb_setup_default(dbName, {});
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function scheduleCardReviewLocal(userDB, review) {
|
|
1699
|
+
const now = moment2.utc();
|
|
1700
|
+
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
1701
|
+
void userDB.put({
|
|
1702
|
+
_id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT2),
|
|
1703
|
+
cardId: review.card_id,
|
|
1704
|
+
reviewTime: review.time.toISOString(),
|
|
1705
|
+
courseId: review.course_id,
|
|
1706
|
+
scheduledAt: now.toISOString(),
|
|
1707
|
+
scheduledFor: review.scheduledFor,
|
|
1708
|
+
schedulingAgentId: review.schedulingAgentId
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
|
|
1712
|
+
const reviewDoc = await userDB.get(reviewDocID);
|
|
1713
|
+
userDB.remove(reviewDoc).then((res) => {
|
|
1714
|
+
if (res.ok) {
|
|
1715
|
+
log2(`Removed Review Doc: ${reviewDocID}`);
|
|
1716
|
+
}
|
|
1717
|
+
}).catch((err) => {
|
|
1718
|
+
log2(`Failed to remove Review Doc: ${reviewDocID},
|
|
1719
|
+
${JSON.stringify(err)}`);
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
var REVIEW_TIME_FORMAT2, log2;
|
|
1723
|
+
var init_userDBHelpers = __esm({
|
|
1724
|
+
"src/impl/common/userDBHelpers.ts"() {
|
|
1725
|
+
"use strict";
|
|
1726
|
+
init_core();
|
|
1727
|
+
init_logger();
|
|
1728
|
+
init_pouchdb_setup();
|
|
1729
|
+
init_dataDirectory();
|
|
1730
|
+
REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
1731
|
+
log2 = (s) => {
|
|
1732
|
+
logger.info(s);
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
// src/impl/couch/user-course-relDB.ts
|
|
1738
|
+
import moment3 from "moment";
|
|
1739
|
+
var UsrCrsData;
|
|
1740
|
+
var init_user_course_relDB = __esm({
|
|
1741
|
+
"src/impl/couch/user-course-relDB.ts"() {
|
|
1742
|
+
"use strict";
|
|
1743
|
+
init_logger();
|
|
1744
|
+
UsrCrsData = class {
|
|
1745
|
+
user;
|
|
1746
|
+
_courseId;
|
|
1747
|
+
constructor(user, courseId) {
|
|
1748
|
+
this.user = user;
|
|
1749
|
+
this._courseId = courseId;
|
|
1794
1750
|
}
|
|
1795
1751
|
async getReviewsForcast(daysCount) {
|
|
1796
1752
|
const time = moment3.utc().add(daysCount, "days");
|
|
1797
1753
|
return this.getReviewstoDate(time);
|
|
1798
1754
|
}
|
|
1799
|
-
async getPendingReviews(
|
|
1755
|
+
async getPendingReviews() {
|
|
1800
1756
|
const now = moment3.utc();
|
|
1801
|
-
return this.getReviewstoDate(now
|
|
1802
|
-
}
|
|
1803
|
-
async getScheduledReviewCount(course_id) {
|
|
1804
|
-
return (await this.getPendingReviews(course_id)).length;
|
|
1757
|
+
return this.getReviewstoDate(now);
|
|
1805
1758
|
}
|
|
1806
|
-
async
|
|
1807
|
-
|
|
1808
|
-
return regDoc.courses.filter((c) => {
|
|
1809
|
-
return !c.status || c.status === "active" || c.status === "maintenance-mode";
|
|
1810
|
-
});
|
|
1759
|
+
async getScheduledReviewCount() {
|
|
1760
|
+
return (await this.getPendingReviews()).length;
|
|
1811
1761
|
}
|
|
1812
|
-
async
|
|
1813
|
-
const
|
|
1814
|
-
const
|
|
1815
|
-
if (
|
|
1816
|
-
return
|
|
1762
|
+
async getCourseSettings() {
|
|
1763
|
+
const regDoc = await this.user.getCourseRegistrationsDoc();
|
|
1764
|
+
const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
|
|
1765
|
+
if (crsDoc && crsDoc.settings) {
|
|
1766
|
+
return crsDoc.settings;
|
|
1817
1767
|
} else {
|
|
1818
|
-
|
|
1768
|
+
logger.warn(`no settings found during lookup on course ${this._courseId}`);
|
|
1769
|
+
return {};
|
|
1819
1770
|
}
|
|
1820
1771
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
count: 0
|
|
1835
|
-
},
|
|
1836
|
-
tags: {},
|
|
1837
|
-
misc: {}
|
|
1838
|
-
}
|
|
1839
|
-
};
|
|
1840
|
-
if (doc.courses.filter((course) => {
|
|
1841
|
-
return course.courseID === regItem.courseID;
|
|
1842
|
-
}).length === 0) {
|
|
1843
|
-
log3(`It's a new course registration!`);
|
|
1844
|
-
doc.courses.push(regItem);
|
|
1845
|
-
doc.studyWeight[course_id] = 1;
|
|
1846
|
-
} else {
|
|
1847
|
-
doc.courses.forEach((c) => {
|
|
1848
|
-
log3(`Found the previously registered course!`);
|
|
1849
|
-
if (c.courseID === course_id) {
|
|
1850
|
-
c.status = status;
|
|
1851
|
-
}
|
|
1852
|
-
});
|
|
1853
|
-
}
|
|
1854
|
-
return this.localDB.put(doc);
|
|
1855
|
-
}).catch((e) => {
|
|
1856
|
-
log3(`Registration failed because of: ${JSON.stringify(e)}`);
|
|
1857
|
-
throw e;
|
|
1772
|
+
updateCourseSettings(updates) {
|
|
1773
|
+
if ("updateCourseSettings" in this.user) {
|
|
1774
|
+
void this.user.updateCourseSettings(this._courseId, updates);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
async getReviewstoDate(targetDate) {
|
|
1778
|
+
const allReviews = await this.user.getPendingReviews(this._courseId);
|
|
1779
|
+
logger.debug(
|
|
1780
|
+
`Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
|
|
1781
|
+
);
|
|
1782
|
+
return allReviews.filter((review) => {
|
|
1783
|
+
const reviewTime = moment3.utc(review.reviewTime);
|
|
1784
|
+
return targetDate.isAfter(reviewTime);
|
|
1858
1785
|
});
|
|
1859
1786
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// src/impl/common/BaseUserDB.ts
|
|
1792
|
+
import { Status as Status3 } from "@vue-skuilder/common";
|
|
1793
|
+
import moment4 from "moment";
|
|
1794
|
+
async function getOrCreateClassroomRegistrationsDoc(user) {
|
|
1795
|
+
let ret;
|
|
1796
|
+
try {
|
|
1797
|
+
ret = await getLocalUserDB(user).get(userClassroomsDoc);
|
|
1798
|
+
} catch (e) {
|
|
1799
|
+
const err = e;
|
|
1800
|
+
if (err.status === 404) {
|
|
1801
|
+
await getLocalUserDB(user).put({
|
|
1802
|
+
_id: userClassroomsDoc,
|
|
1803
|
+
registrations: []
|
|
1804
|
+
});
|
|
1805
|
+
ret = await getOrCreateClassroomRegistrationsDoc(user);
|
|
1806
|
+
} else {
|
|
1807
|
+
const errorDetails = {
|
|
1808
|
+
name: err.name,
|
|
1809
|
+
status: err.status,
|
|
1810
|
+
message: err.message,
|
|
1811
|
+
reason: err.reason,
|
|
1812
|
+
error: err.error
|
|
1813
|
+
};
|
|
1814
|
+
logger.error(
|
|
1815
|
+
"Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
|
|
1816
|
+
errorDetails
|
|
1817
|
+
);
|
|
1818
|
+
throw new Error(
|
|
1819
|
+
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return ret;
|
|
1824
|
+
}
|
|
1825
|
+
async function getOrCreateCourseRegistrationsDoc(user) {
|
|
1826
|
+
let ret;
|
|
1827
|
+
try {
|
|
1828
|
+
ret = await getLocalUserDB(user).get(userCoursesDoc);
|
|
1829
|
+
} catch (e) {
|
|
1830
|
+
const err = e;
|
|
1831
|
+
if (err.status === 404) {
|
|
1832
|
+
await getLocalUserDB(user).put({
|
|
1833
|
+
_id: userCoursesDoc,
|
|
1834
|
+
courses: [],
|
|
1835
|
+
studyWeight: {}
|
|
1836
|
+
});
|
|
1837
|
+
ret = await getOrCreateCourseRegistrationsDoc(user);
|
|
1838
|
+
} else {
|
|
1839
|
+
throw new Error(
|
|
1840
|
+
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return ret;
|
|
1845
|
+
}
|
|
1846
|
+
async function updateUserElo(user, course_id, elo) {
|
|
1847
|
+
const regDoc = await getOrCreateCourseRegistrationsDoc(user);
|
|
1848
|
+
const course = regDoc.courses.find((c) => c.courseID === course_id);
|
|
1849
|
+
course.elo = elo;
|
|
1850
|
+
return getLocalUserDB(user).put(regDoc);
|
|
1851
|
+
}
|
|
1852
|
+
async function registerUserForClassroom(user, classID, registerAs) {
|
|
1853
|
+
log3(`Registering user: ${user} in course: ${classID}`);
|
|
1854
|
+
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
1855
|
+
const regItem = {
|
|
1856
|
+
classID,
|
|
1857
|
+
registeredAs: registerAs
|
|
1858
|
+
};
|
|
1859
|
+
if (doc.registrations.filter((reg) => {
|
|
1860
|
+
return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
|
|
1861
|
+
}).length === 0) {
|
|
1862
|
+
doc.registrations.push(regItem);
|
|
1863
|
+
} else {
|
|
1864
|
+
log3(`User ${user} is already registered for class ${classID}`);
|
|
1865
|
+
}
|
|
1866
|
+
return getLocalUserDB(user).put(doc);
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
async function dropUserFromClassroom(user, classID) {
|
|
1870
|
+
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
1871
|
+
let index = -1;
|
|
1872
|
+
for (let i = 0; i < doc.registrations.length; i++) {
|
|
1873
|
+
if (doc.registrations[i].classID === classID) {
|
|
1874
|
+
index = i;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (index !== -1) {
|
|
1878
|
+
doc.registrations.splice(index, 1);
|
|
1879
|
+
}
|
|
1880
|
+
return getLocalUserDB(user).put(doc);
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
async function getUserClassrooms(user) {
|
|
1884
|
+
return getOrCreateClassroomRegistrationsDoc(user);
|
|
1885
|
+
}
|
|
1886
|
+
var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
1887
|
+
var init_BaseUserDB = __esm({
|
|
1888
|
+
"src/impl/common/BaseUserDB.ts"() {
|
|
1889
|
+
"use strict";
|
|
1890
|
+
init_core();
|
|
1891
|
+
init_util();
|
|
1892
|
+
init_types_legacy();
|
|
1893
|
+
init_logger();
|
|
1894
|
+
init_userDBHelpers();
|
|
1895
|
+
init_updateQueue();
|
|
1896
|
+
init_user_course_relDB();
|
|
1897
|
+
init_couch();
|
|
1898
|
+
log3 = (s) => {
|
|
1899
|
+
logger.info(s);
|
|
1900
|
+
};
|
|
1901
|
+
BaseUser = class _BaseUser {
|
|
1902
|
+
static _instance;
|
|
1903
|
+
static _initialized = false;
|
|
1904
|
+
static Dummy(syncStrategy) {
|
|
1905
|
+
return new _BaseUser("Me", syncStrategy);
|
|
1906
|
+
}
|
|
1907
|
+
static DOC_IDS = {
|
|
1908
|
+
CONFIG: "CONFIG",
|
|
1909
|
+
COURSE_REGISTRATIONS: "CourseRegistrations",
|
|
1910
|
+
CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
|
|
1911
|
+
};
|
|
1912
|
+
// private email: string;
|
|
1913
|
+
_username;
|
|
1914
|
+
syncStrategy;
|
|
1915
|
+
getUsername() {
|
|
1916
|
+
return this._username;
|
|
1878
1917
|
}
|
|
1879
|
-
|
|
1880
|
-
return
|
|
1918
|
+
isLoggedIn() {
|
|
1919
|
+
return !this._username.startsWith(GuestUsername);
|
|
1881
1920
|
}
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
const registeredCourses = await this.getCourseRegistrationsDoc();
|
|
1885
|
-
courseIDs = courseIDs.concat(
|
|
1886
|
-
registeredCourses.courses.map((course) => {
|
|
1887
|
-
return course.courseID;
|
|
1888
|
-
})
|
|
1889
|
-
);
|
|
1890
|
-
const cfgs = await Promise.all(
|
|
1891
|
-
courseIDs.map(async (id) => {
|
|
1892
|
-
return await getCredentialledCourseConfig(id);
|
|
1893
|
-
})
|
|
1894
|
-
);
|
|
1895
|
-
return cfgs;
|
|
1921
|
+
remote() {
|
|
1922
|
+
return this.remoteDB;
|
|
1896
1923
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
logger.debug("Raw config from DB:", cfg);
|
|
1906
|
-
return cfg;
|
|
1907
|
-
} catch (e) {
|
|
1908
|
-
const err = e;
|
|
1909
|
-
if (err.name && err.name === "not_found") {
|
|
1910
|
-
await this.localDB.put(defaultConfig);
|
|
1911
|
-
return this.getConfig();
|
|
1912
|
-
} else {
|
|
1913
|
-
logger.error(`Error setting user default config:`, e);
|
|
1914
|
-
throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
|
|
1915
|
-
}
|
|
1924
|
+
localDB;
|
|
1925
|
+
remoteDB;
|
|
1926
|
+
writeDB;
|
|
1927
|
+
// Database to use for write operations (local-first approach)
|
|
1928
|
+
updateQueue;
|
|
1929
|
+
async createAccount(username, password) {
|
|
1930
|
+
if (!this.syncStrategy.canCreateAccount()) {
|
|
1931
|
+
throw new Error("Account creation not supported by current sync strategy");
|
|
1916
1932
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
...c,
|
|
1923
|
-
...items
|
|
1924
|
-
});
|
|
1925
|
-
if (put.ok) {
|
|
1926
|
-
logger.debug(`Config items set: ${JSON.stringify(items)}`);
|
|
1927
|
-
} else {
|
|
1928
|
-
logger.error(`Error setting config items: ${JSON.stringify(put)}`);
|
|
1933
|
+
if (!this._username.startsWith(GuestUsername)) {
|
|
1934
|
+
throw new Error(
|
|
1935
|
+
`Cannot create a new account while logged in:
|
|
1936
|
+
Currently logged-in as ${this._username}.`
|
|
1937
|
+
);
|
|
1929
1938
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
*/
|
|
1941
|
-
static async instance(syncStrategy, username) {
|
|
1942
|
-
if (username) {
|
|
1943
|
-
_BaseUser._instance = new _BaseUser(username, syncStrategy);
|
|
1944
|
-
await _BaseUser._instance.init();
|
|
1945
|
-
return _BaseUser._instance;
|
|
1946
|
-
} else if (_BaseUser._instance && _BaseUser._initialized) {
|
|
1947
|
-
return _BaseUser._instance;
|
|
1948
|
-
} else if (_BaseUser._instance) {
|
|
1949
|
-
return new Promise((resolve) => {
|
|
1950
|
-
(function waitForUser() {
|
|
1951
|
-
if (_BaseUser._initialized) {
|
|
1952
|
-
return resolve(_BaseUser._instance);
|
|
1953
|
-
} else {
|
|
1954
|
-
setTimeout(waitForUser, 50);
|
|
1955
|
-
}
|
|
1956
|
-
})();
|
|
1957
|
-
});
|
|
1958
|
-
} else {
|
|
1959
|
-
const guestUsername = await syncStrategy.getCurrentUsername();
|
|
1960
|
-
_BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
|
|
1961
|
-
await _BaseUser._instance.init();
|
|
1962
|
-
return _BaseUser._instance;
|
|
1939
|
+
const result = await this.syncStrategy.createAccount(username, password);
|
|
1940
|
+
if (result.status === Status3.ok) {
|
|
1941
|
+
log3(`Account created successfully, updating username to ${username}`);
|
|
1942
|
+
this._username = username;
|
|
1943
|
+
try {
|
|
1944
|
+
localStorage.removeItem("dbUUID");
|
|
1945
|
+
} catch (e) {
|
|
1946
|
+
logger.warn("localStorage not available (Node.js environment):", e);
|
|
1947
|
+
}
|
|
1948
|
+
await this.init();
|
|
1963
1949
|
}
|
|
1950
|
+
return {
|
|
1951
|
+
status: result.status,
|
|
1952
|
+
error: result.error || ""
|
|
1953
|
+
};
|
|
1964
1954
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
this.syncStrategy = syncStrategy;
|
|
1969
|
-
this.setDBandQ();
|
|
1970
|
-
}
|
|
1971
|
-
setDBandQ() {
|
|
1972
|
-
this.localDB = getLocalUserDB(this._username);
|
|
1973
|
-
this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
|
|
1974
|
-
this.updateQueue = new UpdateQueue(this.localDB);
|
|
1975
|
-
}
|
|
1976
|
-
async init() {
|
|
1977
|
-
_BaseUser._initialized = false;
|
|
1978
|
-
if (this._username === "admin") {
|
|
1979
|
-
_BaseUser._initialized = true;
|
|
1980
|
-
return;
|
|
1955
|
+
async login(username, password) {
|
|
1956
|
+
if (!this.syncStrategy.canAuthenticate()) {
|
|
1957
|
+
throw new Error("Authentication not supported by current sync strategy");
|
|
1981
1958
|
}
|
|
1982
|
-
this.
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
_BaseUser._initialized = true;
|
|
1987
|
-
}
|
|
1988
|
-
static designDocs = [
|
|
1989
|
-
{
|
|
1990
|
-
_id: "_design/reviewCards",
|
|
1991
|
-
views: {
|
|
1992
|
-
reviewCards: {
|
|
1993
|
-
map: `function (doc) {
|
|
1994
|
-
if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
|
|
1995
|
-
emit(doc._id, doc.courseId + '-' + doc.cardId);
|
|
1996
|
-
}
|
|
1997
|
-
}`
|
|
1998
|
-
}
|
|
1959
|
+
if (!this._username.startsWith(GuestUsername) && this._username != username) {
|
|
1960
|
+
if (this._username != username) {
|
|
1961
|
+
throw new Error(`Cannot change accounts while logged in.
|
|
1962
|
+
Log out of account ${this.getUsername()} before logging in as ${username}.`);
|
|
1999
1963
|
}
|
|
1964
|
+
logger.warn(`User ${this._username} is already logged in, but executing login again.`);
|
|
2000
1965
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
}
|
|
2006
|
-
for (const doc of _BaseUser.designDocs) {
|
|
1966
|
+
const loginResult = await this.syncStrategy.authenticate(username, password);
|
|
1967
|
+
if (loginResult.ok) {
|
|
1968
|
+
log3(`Logged in as ${username}`);
|
|
1969
|
+
this._username = username;
|
|
2007
1970
|
try {
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
1971
|
+
localStorage.removeItem("dbUUID");
|
|
1972
|
+
} catch (e) {
|
|
1973
|
+
logger.warn("localStorage not available (Node.js environment):", e);
|
|
1974
|
+
}
|
|
1975
|
+
await this.init();
|
|
1976
|
+
}
|
|
1977
|
+
return loginResult;
|
|
1978
|
+
}
|
|
1979
|
+
async resetUserData() {
|
|
1980
|
+
if (this.syncStrategy.canAuthenticate()) {
|
|
1981
|
+
return {
|
|
1982
|
+
status: Status3.error,
|
|
1983
|
+
error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
try {
|
|
1987
|
+
const localDB = getLocalUserDB(this._username);
|
|
1988
|
+
const allDocs = await localDB.allDocs({ include_docs: false });
|
|
1989
|
+
const docsToDelete = allDocs.rows.filter((row) => {
|
|
1990
|
+
const id = row.id;
|
|
1991
|
+
return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
|
|
1992
|
+
id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
|
|
1993
|
+
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
1994
|
+
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
1995
|
+
id === _BaseUser.DOC_IDS.CONFIG;
|
|
1996
|
+
}).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
|
|
1997
|
+
if (docsToDelete.length > 0) {
|
|
1998
|
+
await localDB.bulkDocs(docsToDelete);
|
|
2030
1999
|
}
|
|
2000
|
+
await this.init();
|
|
2001
|
+
return { status: Status3.ok };
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
logger.error("Failed to reset user data:", error);
|
|
2004
|
+
return {
|
|
2005
|
+
status: Status3.error,
|
|
2006
|
+
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
async logout() {
|
|
2011
|
+
if (!this.syncStrategy.canAuthenticate()) {
|
|
2012
|
+
this._username = await this.syncStrategy.getCurrentUsername();
|
|
2013
|
+
await this.init();
|
|
2014
|
+
return { ok: true };
|
|
2031
2015
|
}
|
|
2016
|
+
const ret = await this.syncStrategy.logout();
|
|
2017
|
+
this._username = await this.syncStrategy.getCurrentUsername();
|
|
2018
|
+
await this.init();
|
|
2019
|
+
return ret;
|
|
2032
2020
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2021
|
+
update(id, update) {
|
|
2022
|
+
return this.updateQueue.update(id, update);
|
|
2023
|
+
}
|
|
2024
|
+
async getCourseRegistrationsDoc() {
|
|
2025
|
+
logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
|
|
2026
|
+
let ret;
|
|
2035
2027
|
try {
|
|
2036
|
-
const
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
});
|
|
2028
|
+
const regDoc = await this.localDB.get(
|
|
2029
|
+
_BaseUser.DOC_IDS.COURSE_REGISTRATIONS
|
|
2030
|
+
);
|
|
2031
|
+
return regDoc;
|
|
2041
2032
|
} catch (e) {
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2033
|
+
const err = e;
|
|
2034
|
+
if (err.status === 404) {
|
|
2035
|
+
await this.localDB.put({
|
|
2036
|
+
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
2037
|
+
courses: [],
|
|
2038
|
+
studyWeight: {}
|
|
2039
|
+
});
|
|
2040
|
+
ret = await this.getCourseRegistrationsDoc();
|
|
2041
|
+
} else {
|
|
2042
|
+
throw new Error(
|
|
2043
|
+
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
2044
|
+
);
|
|
2045
2045
|
}
|
|
2046
|
-
throw e;
|
|
2047
2046
|
}
|
|
2047
|
+
return ret;
|
|
2048
|
+
}
|
|
2049
|
+
async getActiveCourses() {
|
|
2050
|
+
const reg = await this.getCourseRegistrationsDoc();
|
|
2051
|
+
return reg.courses.filter((c) => {
|
|
2052
|
+
return c.status === void 0 || c.status === "active";
|
|
2053
|
+
});
|
|
2048
2054
|
}
|
|
2049
2055
|
/**
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
2053
|
-
* // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
|
|
2056
|
+
* Returns a promise of the card IDs that the user has
|
|
2057
|
+
* a scheduled review for.
|
|
2054
2058
|
*
|
|
2055
|
-
* @param record the recent recorded interaction between user and card
|
|
2056
|
-
* @returns The updated state of the card's CardHistory data
|
|
2057
2059
|
*/
|
|
2058
|
-
async
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2060
|
+
async getActiveCards() {
|
|
2061
|
+
const keys = getStartAndEndKeys2(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
|
|
2062
|
+
const reviews = await this.remoteDB.allDocs({
|
|
2063
|
+
startkey: keys.startkey,
|
|
2064
|
+
endkey: keys.endkey,
|
|
2065
|
+
include_docs: true
|
|
2066
|
+
});
|
|
2067
|
+
return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
|
|
2068
|
+
}
|
|
2069
|
+
async getActivityRecords() {
|
|
2061
2070
|
try {
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2071
|
+
const hist = await this.getHistory();
|
|
2072
|
+
const allRecords = [];
|
|
2073
|
+
if (!Array.isArray(hist)) {
|
|
2074
|
+
logger.error("getHistory did not return an array:", hist);
|
|
2075
|
+
return allRecords;
|
|
2076
|
+
}
|
|
2077
|
+
let sampleCount = 0;
|
|
2078
|
+
for (let i = 0; i < hist.length; i++) {
|
|
2079
|
+
try {
|
|
2080
|
+
if (hist[i] && Array.isArray(hist[i].records)) {
|
|
2081
|
+
hist[i].records.forEach((record) => {
|
|
2082
|
+
try {
|
|
2083
|
+
if (!record.timeStamp) {
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
let timeStamp;
|
|
2087
|
+
if (typeof record.timeStamp === "object") {
|
|
2088
|
+
if (typeof record.timeStamp.toDate === "function") {
|
|
2089
|
+
timeStamp = record.timeStamp.toISOString();
|
|
2090
|
+
} else if (record.timeStamp instanceof Date) {
|
|
2091
|
+
timeStamp = record.timeStamp.toISOString();
|
|
2092
|
+
} else {
|
|
2093
|
+
if (sampleCount < 3) {
|
|
2094
|
+
logger.warn("Unknown timestamp object type:", record.timeStamp);
|
|
2095
|
+
sampleCount++;
|
|
2096
|
+
}
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
} else if (typeof record.timeStamp === "string") {
|
|
2100
|
+
const date = new Date(record.timeStamp);
|
|
2101
|
+
if (isNaN(date.getTime())) {
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
timeStamp = record.timeStamp;
|
|
2105
|
+
} else if (typeof record.timeStamp === "number") {
|
|
2106
|
+
timeStamp = new Date(record.timeStamp).toISOString();
|
|
2107
|
+
} else {
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
allRecords.push({
|
|
2111
|
+
timeStamp,
|
|
2112
|
+
courseID: record.courseID || "unknown",
|
|
2113
|
+
cardID: record.cardID || "unknown",
|
|
2114
|
+
timeSpent: record.timeSpent || 0,
|
|
2115
|
+
type: "card_view"
|
|
2116
|
+
});
|
|
2117
|
+
} catch (err) {
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
} catch (err) {
|
|
2122
|
+
logger.error("Error processing history item:", err);
|
|
2070
2123
|
}
|
|
2071
|
-
);
|
|
2072
|
-
cardHistory.records = cardHistory.records.map((record2) => {
|
|
2073
|
-
const ret = {
|
|
2074
|
-
...record2
|
|
2075
|
-
};
|
|
2076
|
-
ret.timeStamp = moment3.utc(record2.timeStamp);
|
|
2077
|
-
return ret;
|
|
2078
|
-
});
|
|
2079
|
-
return cardHistory;
|
|
2080
|
-
} catch (e) {
|
|
2081
|
-
const reason = e;
|
|
2082
|
-
if (reason.status === 404) {
|
|
2083
|
-
const initCardHistory = {
|
|
2084
|
-
_id: cardHistoryID,
|
|
2085
|
-
cardID: record.cardID,
|
|
2086
|
-
courseID: record.courseID,
|
|
2087
|
-
records: [record],
|
|
2088
|
-
lapses: 0,
|
|
2089
|
-
streak: 0,
|
|
2090
|
-
bestInterval: 0
|
|
2091
|
-
};
|
|
2092
|
-
void this.remoteDB.put(initCardHistory);
|
|
2093
|
-
return initCardHistory;
|
|
2094
|
-
} else {
|
|
2095
|
-
throw new Error(`putCardRecord failed because of:
|
|
2096
|
-
name:${reason.name}
|
|
2097
|
-
error: ${reason.error}
|
|
2098
|
-
message: ${reason.message}`);
|
|
2099
2124
|
}
|
|
2125
|
+
logger.debug(`Found ${allRecords.length} activity records`);
|
|
2126
|
+
return allRecords;
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
logger.error("Error in getActivityRecords:", err);
|
|
2129
|
+
return [];
|
|
2100
2130
|
}
|
|
2101
2131
|
}
|
|
2102
|
-
async
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
}
|
|
2122
|
-
});
|
|
2123
|
-
if (duplicateDocIds.length > 0) {
|
|
2124
|
-
log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
|
|
2125
|
-
const deletePromises = duplicateDocIds.map(async (docId) => {
|
|
2126
|
-
try {
|
|
2127
|
-
const doc = await this.remoteDB.get(docId);
|
|
2128
|
-
await this.remoteDB.remove(doc);
|
|
2129
|
-
log3(`Successfully removed duplicate review: ${docId}`);
|
|
2130
|
-
} catch (error) {
|
|
2131
|
-
log3(`Failed to remove duplicate review ${docId}: ${error}`);
|
|
2132
|
+
async getReviewstoDate(targetDate, course_id) {
|
|
2133
|
+
const keys = getStartAndEndKeys2(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]);
|
|
2134
|
+
const reviews = await this.remoteDB.allDocs({
|
|
2135
|
+
startkey: keys.startkey,
|
|
2136
|
+
endkey: keys.endkey,
|
|
2137
|
+
include_docs: true
|
|
2138
|
+
});
|
|
2139
|
+
log3(
|
|
2140
|
+
`Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
|
|
2141
|
+
);
|
|
2142
|
+
return reviews.rows.filter((r) => {
|
|
2143
|
+
if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
|
|
2144
|
+
const date = moment4.utc(
|
|
2145
|
+
r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
|
|
2146
|
+
REVIEW_TIME_FORMAT2
|
|
2147
|
+
);
|
|
2148
|
+
if (targetDate.isAfter(date)) {
|
|
2149
|
+
if (course_id === void 0 || r.doc.courseId === course_id) {
|
|
2150
|
+
return true;
|
|
2132
2151
|
}
|
|
2133
|
-
}
|
|
2134
|
-
await Promise.all(deletePromises);
|
|
2135
|
-
log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
|
|
2136
|
-
} else {
|
|
2137
|
-
log3("No duplicate reviews found");
|
|
2152
|
+
}
|
|
2138
2153
|
}
|
|
2139
|
-
}
|
|
2140
|
-
log3(`Error during review deduplication: ${error}`);
|
|
2141
|
-
}
|
|
2154
|
+
}).map((r) => r.doc);
|
|
2142
2155
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
+
async getReviewsForcast(daysCount) {
|
|
2157
|
+
const time = moment4.utc().add(daysCount, "days");
|
|
2158
|
+
return this.getReviewstoDate(time);
|
|
2159
|
+
}
|
|
2160
|
+
async getPendingReviews(course_id) {
|
|
2161
|
+
const now = moment4.utc();
|
|
2162
|
+
return this.getReviewstoDate(now, course_id);
|
|
2163
|
+
}
|
|
2164
|
+
async getScheduledReviewCount(course_id) {
|
|
2165
|
+
return (await this.getPendingReviews(course_id)).length;
|
|
2166
|
+
}
|
|
2167
|
+
async getRegisteredCourses() {
|
|
2168
|
+
const regDoc = await this.getCourseRegistrationsDoc();
|
|
2169
|
+
return regDoc.courses.filter((c) => {
|
|
2170
|
+
return !c.status || c.status === "active" || c.status === "maintenance-mode";
|
|
2156
2171
|
});
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2172
|
+
}
|
|
2173
|
+
async getCourseRegDoc(courseID) {
|
|
2174
|
+
const regDocs = await this.getCourseRegistrationsDoc();
|
|
2175
|
+
const ret = regDocs.courses.find((c) => c.courseID === courseID);
|
|
2176
|
+
if (ret) {
|
|
2177
|
+
return ret;
|
|
2178
|
+
} else {
|
|
2179
|
+
throw new Error(`Course registration not found for course ID: ${courseID}`);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
async registerForCourse(course_id, previewMode = false) {
|
|
2183
|
+
return this.getCourseRegistrationsDoc().then((doc) => {
|
|
2184
|
+
const status = previewMode ? "preview" : "active";
|
|
2185
|
+
logger.debug(`Registering for ${course_id} with status: ${status}`);
|
|
2186
|
+
const regItem = {
|
|
2187
|
+
status,
|
|
2188
|
+
courseID: course_id,
|
|
2189
|
+
user: true,
|
|
2190
|
+
admin: false,
|
|
2191
|
+
moderator: false,
|
|
2192
|
+
elo: {
|
|
2193
|
+
global: {
|
|
2194
|
+
score: 1e3,
|
|
2195
|
+
count: 0
|
|
2196
|
+
},
|
|
2197
|
+
tags: {},
|
|
2198
|
+
misc: {}
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
if (doc.courses.filter((course) => {
|
|
2202
|
+
return course.courseID === regItem.courseID;
|
|
2203
|
+
}).length === 0) {
|
|
2204
|
+
log3(`It's a new course registration!`);
|
|
2205
|
+
doc.courses.push(regItem);
|
|
2206
|
+
doc.studyWeight[course_id] = 1;
|
|
2207
|
+
} else {
|
|
2208
|
+
doc.courses.forEach((c) => {
|
|
2209
|
+
log3(`Found the previously registered course!`);
|
|
2210
|
+
if (c.courseID === course_id) {
|
|
2211
|
+
c.status = status;
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2161
2214
|
}
|
|
2215
|
+
return this.localDB.put(doc);
|
|
2216
|
+
}).catch((e) => {
|
|
2217
|
+
log3(`Registration failed because of: ${JSON.stringify(e)}`);
|
|
2218
|
+
throw e;
|
|
2162
2219
|
});
|
|
2163
|
-
return ret;
|
|
2164
|
-
}
|
|
2165
|
-
/**
|
|
2166
|
-
*
|
|
2167
|
-
* @returns A promise of the cards that the user has seen in the past.
|
|
2168
|
-
*/
|
|
2169
|
-
async getHistory() {
|
|
2170
|
-
const cards = await filterAllDocsByPrefix(
|
|
2171
|
-
this.remoteDB,
|
|
2172
|
-
cardHistoryPrefix2,
|
|
2173
|
-
{
|
|
2174
|
-
include_docs: true,
|
|
2175
|
-
attachments: false
|
|
2176
|
-
}
|
|
2177
|
-
);
|
|
2178
|
-
return cards.rows.map((r) => r.doc);
|
|
2179
2220
|
}
|
|
2180
|
-
async
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
if (
|
|
2185
|
-
|
|
2221
|
+
async dropCourse(course_id, dropStatus = "dropped") {
|
|
2222
|
+
return this.getCourseRegistrationsDoc().then((doc) => {
|
|
2223
|
+
let index = -1;
|
|
2224
|
+
for (let i = 0; i < doc.courses.length; i++) {
|
|
2225
|
+
if (doc.courses[i].courseID === course_id) {
|
|
2226
|
+
index = i;
|
|
2186
2227
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2228
|
+
}
|
|
2229
|
+
if (index !== -1) {
|
|
2230
|
+
delete doc.studyWeight[course_id];
|
|
2231
|
+
doc.courses[index].status = dropStatus;
|
|
2232
|
+
} else {
|
|
2233
|
+
throw new Error(
|
|
2234
|
+
`User ${this.getUsername()} is not currently registered for course ${course_id}`
|
|
2235
|
+
);
|
|
2190
2236
|
}
|
|
2191
2237
|
return this.localDB.put(doc);
|
|
2192
2238
|
});
|
|
2193
2239
|
}
|
|
2194
|
-
async
|
|
2195
|
-
|
|
2196
|
-
const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
|
|
2197
|
-
if (crsDoc) {
|
|
2198
|
-
return crsDoc.settings;
|
|
2199
|
-
} else {
|
|
2200
|
-
throw new Error(`getCourseSettings Failed:
|
|
2201
|
-
User is not registered for course ${course_id}`);
|
|
2202
|
-
}
|
|
2240
|
+
async getCourseInterface(courseId) {
|
|
2241
|
+
return new UsrCrsData(this, courseId);
|
|
2203
2242
|
}
|
|
2204
|
-
async
|
|
2205
|
-
let
|
|
2243
|
+
async getUserEditableCourses() {
|
|
2244
|
+
let courseIDs = [];
|
|
2245
|
+
const registeredCourses = await this.getCourseRegistrationsDoc();
|
|
2246
|
+
courseIDs = courseIDs.concat(
|
|
2247
|
+
registeredCourses.courses.map((course) => {
|
|
2248
|
+
return course.courseID;
|
|
2249
|
+
})
|
|
2250
|
+
);
|
|
2251
|
+
const cfgs = await Promise.all(
|
|
2252
|
+
courseIDs.map(async (id) => {
|
|
2253
|
+
return await getCredentialledCourseConfig(id);
|
|
2254
|
+
})
|
|
2255
|
+
);
|
|
2256
|
+
return cfgs;
|
|
2257
|
+
}
|
|
2258
|
+
async getConfig() {
|
|
2259
|
+
const defaultConfig = {
|
|
2260
|
+
_id: _BaseUser.DOC_IDS.CONFIG,
|
|
2261
|
+
darkMode: false,
|
|
2262
|
+
likesConfetti: false,
|
|
2263
|
+
sessionTimeLimit: 5
|
|
2264
|
+
};
|
|
2206
2265
|
try {
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2266
|
+
const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
|
|
2267
|
+
logger.debug("Raw config from DB:", cfg);
|
|
2268
|
+
return cfg;
|
|
2210
2269
|
} catch (e) {
|
|
2211
2270
|
const err = e;
|
|
2212
|
-
if (err.
|
|
2213
|
-
await this.
|
|
2214
|
-
|
|
2215
|
-
registrations: []
|
|
2216
|
-
});
|
|
2217
|
-
ret = await this.getOrCreateClassroomRegistrationsDoc();
|
|
2271
|
+
if (err.name && err.name === "not_found") {
|
|
2272
|
+
await this.localDB.put(defaultConfig);
|
|
2273
|
+
return this.getConfig();
|
|
2218
2274
|
} else {
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
status: err.status,
|
|
2222
|
-
message: err.message,
|
|
2223
|
-
reason: err.reason,
|
|
2224
|
-
error: err.error
|
|
2225
|
-
};
|
|
2226
|
-
logger.error(
|
|
2227
|
-
"Database error in getOrCreateClassroomRegistrationsDoc (private method):",
|
|
2228
|
-
errorDetails
|
|
2229
|
-
);
|
|
2230
|
-
throw new Error(
|
|
2231
|
-
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
2232
|
-
);
|
|
2275
|
+
logger.error(`Error setting user default config:`, e);
|
|
2276
|
+
throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
|
|
2233
2277
|
}
|
|
2234
2278
|
}
|
|
2235
|
-
logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
|
|
2236
|
-
return ret;
|
|
2237
|
-
}
|
|
2238
|
-
/**
|
|
2239
|
-
* Retrieves the list of active classroom IDs where the user is registered as a student.
|
|
2240
|
-
*
|
|
2241
|
-
* @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
|
|
2242
|
-
* registration document is unavailable due to database errors
|
|
2243
|
-
*
|
|
2244
|
-
* @description This method gracefully handles database connectivity issues by returning
|
|
2245
|
-
* an empty array when the classroom registrations document cannot be accessed.
|
|
2246
|
-
* This ensures that users can still access other application features even
|
|
2247
|
-
* when classroom functionality is temporarily unavailable.
|
|
2248
|
-
*/
|
|
2249
|
-
async getActiveClasses() {
|
|
2250
|
-
try {
|
|
2251
|
-
return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
|
|
2252
|
-
} catch (error) {
|
|
2253
|
-
logger.warn(
|
|
2254
|
-
"Failed to load classroom registrations, continuing without classroom data:",
|
|
2255
|
-
error
|
|
2256
|
-
);
|
|
2257
|
-
return [];
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
async scheduleCardReview(review) {
|
|
2261
|
-
return scheduleCardReviewLocal(this.remoteDB, review);
|
|
2262
|
-
}
|
|
2263
|
-
async removeScheduledCardReview(reviewId) {
|
|
2264
|
-
return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
|
|
2265
|
-
}
|
|
2266
|
-
async registerForClassroom(_classId, _registerAs) {
|
|
2267
|
-
return registerUserForClassroom(this._username, _classId, _registerAs);
|
|
2268
|
-
}
|
|
2269
|
-
async dropFromClassroom(classId) {
|
|
2270
|
-
return dropUserFromClassroom(this._username, classId);
|
|
2271
|
-
}
|
|
2272
|
-
async getUserClassrooms() {
|
|
2273
|
-
return getUserClassrooms(this._username);
|
|
2274
|
-
}
|
|
2275
|
-
async updateUserElo(courseId, elo) {
|
|
2276
|
-
return updateUserElo(this._username, courseId, elo);
|
|
2277
|
-
}
|
|
2278
|
-
};
|
|
2279
|
-
userCoursesDoc = "CourseRegistrations";
|
|
2280
|
-
userClassroomsDoc = "ClassroomRegistrations";
|
|
2281
|
-
}
|
|
2282
|
-
});
|
|
2283
|
-
|
|
2284
|
-
// src/impl/common/index.ts
|
|
2285
|
-
var init_common = __esm({
|
|
2286
|
-
"src/impl/common/index.ts"() {
|
|
2287
|
-
"use strict";
|
|
2288
|
-
init_SyncStrategy();
|
|
2289
|
-
init_BaseUserDB();
|
|
2290
|
-
init_userDBHelpers();
|
|
2291
|
-
}
|
|
2292
|
-
});
|
|
2293
|
-
|
|
2294
|
-
// src/factory.ts
|
|
2295
|
-
function getDataLayer() {
|
|
2296
|
-
if (!dataLayerInstance) {
|
|
2297
|
-
throw new Error("Data layer not initialized. Call initializeDataLayer first.");
|
|
2298
|
-
}
|
|
2299
|
-
return dataLayerInstance;
|
|
2300
|
-
}
|
|
2301
|
-
var ENV, dataLayerInstance;
|
|
2302
|
-
var init_factory = __esm({
|
|
2303
|
-
"src/factory.ts"() {
|
|
2304
|
-
"use strict";
|
|
2305
|
-
init_common();
|
|
2306
|
-
init_logger();
|
|
2307
|
-
ENV = {
|
|
2308
|
-
COUCHDB_SERVER_PROTOCOL: "NOT_SET",
|
|
2309
|
-
COUCHDB_SERVER_URL: "NOT_SET"
|
|
2310
|
-
};
|
|
2311
|
-
dataLayerInstance = null;
|
|
2312
|
-
}
|
|
2313
|
-
});
|
|
2314
|
-
|
|
2315
|
-
// src/impl/couch/classroomDB.ts
|
|
2316
|
-
import moment4 from "moment";
|
|
2317
|
-
function getClassroomDB(classID, version) {
|
|
2318
|
-
const dbName = `classdb-${version}-${classID}`;
|
|
2319
|
-
logger.info(`Retrieving classroom db: ${dbName}`);
|
|
2320
|
-
return new pouchdb_setup_default(
|
|
2321
|
-
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2322
|
-
pouchDBincludeCredentialsConfig
|
|
2323
|
-
);
|
|
2324
|
-
}
|
|
2325
|
-
async function getClassroomConfig(classID) {
|
|
2326
|
-
return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
|
|
2327
|
-
}
|
|
2328
|
-
var classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
|
|
2329
|
-
var init_classroomDB = __esm({
|
|
2330
|
-
"src/impl/couch/classroomDB.ts"() {
|
|
2331
|
-
"use strict";
|
|
2332
|
-
init_factory();
|
|
2333
|
-
init_logger();
|
|
2334
|
-
init_pouchdb_setup();
|
|
2335
|
-
init_couch();
|
|
2336
|
-
init_courseDB();
|
|
2337
|
-
classroomLookupDBTitle = "classdb-lookup";
|
|
2338
|
-
CLASSROOM_CONFIG = "ClassroomConfig";
|
|
2339
|
-
ClassroomDBBase = class {
|
|
2340
|
-
_id;
|
|
2341
|
-
_db;
|
|
2342
|
-
_cfg;
|
|
2343
|
-
_initComplete = false;
|
|
2344
|
-
_content_prefix = "content";
|
|
2345
|
-
get _content_searchkeys() {
|
|
2346
|
-
return getStartAndEndKeys2(this._content_prefix);
|
|
2347
2279
|
}
|
|
2348
|
-
async
|
|
2349
|
-
logger.
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
});
|
|
2355
|
-
const ret = docRows.rows.map((row) => {
|
|
2356
|
-
return row.doc;
|
|
2280
|
+
async setConfig(items) {
|
|
2281
|
+
logger.debug(`Setting Config items ${JSON.stringify(items)}`);
|
|
2282
|
+
const c = await this.getConfig();
|
|
2283
|
+
const put = await this.localDB.put({
|
|
2284
|
+
...c,
|
|
2285
|
+
...items
|
|
2357
2286
|
});
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
getContentId(content) {
|
|
2361
|
-
if (content.type === "tag") {
|
|
2362
|
-
return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
|
|
2287
|
+
if (put.ok) {
|
|
2288
|
+
logger.debug(`Config items set: ${JSON.stringify(items)}`);
|
|
2363
2289
|
} else {
|
|
2364
|
-
|
|
2290
|
+
logger.error(`Error setting config items: ${JSON.stringify(put)}`);
|
|
2365
2291
|
}
|
|
2366
2292
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2293
|
+
/**
|
|
2294
|
+
*
|
|
2295
|
+
* This function should be called *only* by the pouchdb datalayer provider
|
|
2296
|
+
* auth store.
|
|
2297
|
+
*
|
|
2298
|
+
*
|
|
2299
|
+
* Anyone else seeking the current user should use the auth store's
|
|
2300
|
+
* exported `getCurrentUser` method.
|
|
2301
|
+
*
|
|
2302
|
+
*/
|
|
2303
|
+
static async instance(syncStrategy, username) {
|
|
2304
|
+
if (username) {
|
|
2305
|
+
_BaseUser._instance = new _BaseUser(username, syncStrategy);
|
|
2306
|
+
await _BaseUser._instance.init();
|
|
2307
|
+
return _BaseUser._instance;
|
|
2308
|
+
} else if (_BaseUser._instance && _BaseUser._initialized) {
|
|
2309
|
+
return _BaseUser._instance;
|
|
2310
|
+
} else if (_BaseUser._instance) {
|
|
2311
|
+
return new Promise((resolve) => {
|
|
2312
|
+
(function waitForUser() {
|
|
2313
|
+
if (_BaseUser._initialized) {
|
|
2314
|
+
return resolve(_BaseUser._instance);
|
|
2315
|
+
} else {
|
|
2316
|
+
setTimeout(waitForUser, 50);
|
|
2317
|
+
}
|
|
2318
|
+
})();
|
|
2319
|
+
});
|
|
2320
|
+
} else {
|
|
2321
|
+
const guestUsername = await syncStrategy.getCurrentUsername();
|
|
2322
|
+
_BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
|
|
2323
|
+
await _BaseUser._instance.init();
|
|
2324
|
+
return _BaseUser._instance;
|
|
2325
|
+
}
|
|
2369
2326
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2327
|
+
constructor(username, syncStrategy) {
|
|
2328
|
+
_BaseUser._initialized = false;
|
|
2329
|
+
this._username = username;
|
|
2330
|
+
this.syncStrategy = syncStrategy;
|
|
2331
|
+
this.setDBandQ();
|
|
2372
2332
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
constructor(classID, user) {
|
|
2379
|
-
super();
|
|
2380
|
-
this._id = classID;
|
|
2381
|
-
this._user = user;
|
|
2333
|
+
setDBandQ() {
|
|
2334
|
+
this.localDB = getLocalUserDB(this._username);
|
|
2335
|
+
this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
|
|
2336
|
+
this.writeDB = this.syncStrategy.getWriteDB ? this.syncStrategy.getWriteDB(this._username) : this.localDB;
|
|
2337
|
+
this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
|
|
2382
2338
|
}
|
|
2383
2339
|
async init() {
|
|
2384
|
-
|
|
2385
|
-
this.
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2340
|
+
_BaseUser._initialized = false;
|
|
2341
|
+
if (this._username === "admin") {
|
|
2342
|
+
_BaseUser._initialized = true;
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
this.setDBandQ();
|
|
2346
|
+
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
2347
|
+
void this.applyDesignDocs();
|
|
2348
|
+
void this.deduplicateReviews();
|
|
2349
|
+
_BaseUser._initialized = true;
|
|
2350
|
+
}
|
|
2351
|
+
static designDocs = [
|
|
2352
|
+
{
|
|
2353
|
+
_id: "_design/reviewCards",
|
|
2354
|
+
views: {
|
|
2355
|
+
reviewCards: {
|
|
2356
|
+
map: `function (doc) {
|
|
2357
|
+
if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
|
|
2358
|
+
emit(doc._id, doc.courseId + '-' + doc.cardId);
|
|
2359
|
+
}
|
|
2360
|
+
}`
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
];
|
|
2365
|
+
async applyDesignDocs() {
|
|
2366
|
+
if (this._username === "admin") {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
for (const doc of _BaseUser.designDocs) {
|
|
2370
|
+
try {
|
|
2371
|
+
try {
|
|
2372
|
+
const existingDoc = await this.remoteDB.get(doc._id);
|
|
2373
|
+
await this.remoteDB.put({
|
|
2374
|
+
...doc,
|
|
2375
|
+
_rev: existingDoc._rev
|
|
2376
|
+
});
|
|
2377
|
+
} catch (e) {
|
|
2378
|
+
if (e instanceof Error && e.name === "not_found") {
|
|
2379
|
+
await this.remoteDB.put(doc);
|
|
2380
|
+
} else {
|
|
2381
|
+
throw e;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
if (error.name && error.name === "conflict") {
|
|
2386
|
+
logger.warn(`Design doc ${doc._id} update conflict - will retry`);
|
|
2387
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2388
|
+
await this.applyDesignDoc(doc);
|
|
2389
|
+
} else {
|
|
2390
|
+
logger.error(`Failed to apply design doc ${doc._id}:`, error);
|
|
2391
|
+
throw error;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
// Helper method for single doc update with retry
|
|
2397
|
+
async applyDesignDoc(doc, retries = 3) {
|
|
2389
2398
|
try {
|
|
2390
|
-
const
|
|
2391
|
-
this.
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
live: true,
|
|
2395
|
-
include_docs: true
|
|
2399
|
+
const existingDoc = await this.remoteDB.get(doc._id);
|
|
2400
|
+
await this.remoteDB.put({
|
|
2401
|
+
...doc,
|
|
2402
|
+
_rev: existingDoc._rev
|
|
2396
2403
|
});
|
|
2397
|
-
this._initComplete = true;
|
|
2398
|
-
return;
|
|
2399
2404
|
} catch (e) {
|
|
2400
|
-
|
|
2405
|
+
if (e instanceof Error && e.name === "conflict" && retries > 0) {
|
|
2406
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2407
|
+
return this.applyDesignDoc(doc, retries - 1);
|
|
2408
|
+
}
|
|
2409
|
+
throw e;
|
|
2401
2410
|
}
|
|
2402
2411
|
}
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2412
|
+
/**
|
|
2413
|
+
* Logs a record of the user's interaction with the card and returns the card's
|
|
2414
|
+
* up-to-date history
|
|
2415
|
+
*
|
|
2416
|
+
* // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
|
|
2417
|
+
*
|
|
2418
|
+
* @param record the recent recorded interaction between user and card
|
|
2419
|
+
* @returns The updated state of the card's CardHistory data
|
|
2420
|
+
*/
|
|
2421
|
+
async putCardRecord(record) {
|
|
2422
|
+
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
2423
|
+
record.timeStamp = moment4.utc(record.timeStamp).toString();
|
|
2424
|
+
try {
|
|
2425
|
+
const cardHistory = await this.update(
|
|
2426
|
+
cardHistoryID,
|
|
2427
|
+
function(h) {
|
|
2428
|
+
h.records.push(record);
|
|
2429
|
+
h.bestInterval = h.bestInterval || 0;
|
|
2430
|
+
h.lapses = h.lapses || 0;
|
|
2431
|
+
h.streak = h.streak || 0;
|
|
2432
|
+
return h;
|
|
2433
|
+
}
|
|
2434
|
+
);
|
|
2435
|
+
cardHistory.records = cardHistory.records.map((record2) => {
|
|
2436
|
+
const ret = {
|
|
2437
|
+
...record2
|
|
2438
|
+
};
|
|
2439
|
+
ret.timeStamp = moment4.utc(record2.timeStamp);
|
|
2440
|
+
return ret;
|
|
2441
|
+
});
|
|
2442
|
+
return cardHistory;
|
|
2443
|
+
} catch (e) {
|
|
2444
|
+
const reason = e;
|
|
2445
|
+
if (reason.status === 404) {
|
|
2446
|
+
const initCardHistory = {
|
|
2447
|
+
_id: cardHistoryID,
|
|
2448
|
+
cardID: record.cardID,
|
|
2449
|
+
courseID: record.courseID,
|
|
2450
|
+
records: [record],
|
|
2451
|
+
lapses: 0,
|
|
2452
|
+
streak: 0,
|
|
2453
|
+
bestInterval: 0
|
|
2454
|
+
};
|
|
2455
|
+
const putResult = await this.writeDB.put(initCardHistory);
|
|
2456
|
+
return { ...initCardHistory, _rev: putResult.rev };
|
|
2457
|
+
} else {
|
|
2458
|
+
throw new Error(`putCardRecord failed because of:
|
|
2459
|
+
name:${reason.name}
|
|
2460
|
+
error: ${reason.error}
|
|
2461
|
+
message: ${reason.message}`);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2407
2464
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2465
|
+
async deduplicateReviews() {
|
|
2466
|
+
try {
|
|
2467
|
+
log3("Starting deduplication of scheduled reviews...");
|
|
2468
|
+
const reviewsMap = {};
|
|
2469
|
+
const duplicateDocIds = [];
|
|
2470
|
+
const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
|
|
2471
|
+
log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
|
|
2472
|
+
scheduledReviews.rows.forEach((r) => {
|
|
2473
|
+
const qualifiedCardId = r.value;
|
|
2474
|
+
const docId = r.key;
|
|
2475
|
+
if (reviewsMap[qualifiedCardId]) {
|
|
2476
|
+
log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
|
|
2477
|
+
log3(
|
|
2478
|
+
`Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
|
|
2479
|
+
);
|
|
2480
|
+
duplicateDocIds.push(reviewsMap[qualifiedCardId]);
|
|
2481
|
+
reviewsMap[qualifiedCardId] = docId;
|
|
2482
|
+
} else {
|
|
2483
|
+
reviewsMap[qualifiedCardId] = docId;
|
|
2484
|
+
}
|
|
2485
|
+
});
|
|
2486
|
+
if (duplicateDocIds.length > 0) {
|
|
2487
|
+
log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
|
|
2488
|
+
const deletePromises = duplicateDocIds.map(async (docId) => {
|
|
2489
|
+
try {
|
|
2490
|
+
const doc = await this.remoteDB.get(docId);
|
|
2491
|
+
await this.writeDB.remove(doc);
|
|
2492
|
+
log3(`Successfully removed duplicate review: ${docId}`);
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
log3(`Failed to remove duplicate review ${docId}: ${error}`);
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
await Promise.all(deletePromises);
|
|
2498
|
+
log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
|
|
2499
|
+
} else {
|
|
2500
|
+
log3("No duplicate reviews found");
|
|
2501
|
+
}
|
|
2502
|
+
} catch (error) {
|
|
2503
|
+
log3(`Error during review deduplication: ${error}`);
|
|
2504
|
+
}
|
|
2410
2505
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2506
|
+
/**
|
|
2507
|
+
* Returns a promise of the card IDs that the user has
|
|
2508
|
+
* encountered in the past.
|
|
2509
|
+
*
|
|
2510
|
+
* @param course_id optional specification of individual course
|
|
2511
|
+
*/
|
|
2512
|
+
async getSeenCards(course_id) {
|
|
2513
|
+
let prefix = DocTypePrefixes["CARDRECORD" /* CARDRECORD */];
|
|
2514
|
+
if (course_id) {
|
|
2515
|
+
prefix += course_id;
|
|
2516
|
+
}
|
|
2517
|
+
const docs = await filterAllDocsByPrefix2(this.localDB, prefix, {
|
|
2518
|
+
include_docs: false
|
|
2519
|
+
});
|
|
2520
|
+
const ret = [];
|
|
2521
|
+
docs.rows.forEach((row) => {
|
|
2522
|
+
if (row.id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */])) {
|
|
2523
|
+
ret.push(row.id.substr(DocTypePrefixes["CARDRECORD" /* CARDRECORD */].length));
|
|
2524
|
+
}
|
|
2424
2525
|
});
|
|
2526
|
+
return ret;
|
|
2425
2527
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
ret = ret.concat(await db.getNewCards());
|
|
2438
|
-
} else if (content.type === "tag") {
|
|
2439
|
-
const tagDoc = await getTag(content.courseID, content.tagID);
|
|
2440
|
-
ret = ret.concat(
|
|
2441
|
-
tagDoc.taggedCards.map((c) => {
|
|
2442
|
-
return {
|
|
2443
|
-
courseID: content.courseID,
|
|
2444
|
-
cardID: c,
|
|
2445
|
-
qualifiedID: `${content.courseID}-${c}`,
|
|
2446
|
-
contentSourceType: "classroom",
|
|
2447
|
-
contentSourceID: this._id,
|
|
2448
|
-
status: "new"
|
|
2449
|
-
};
|
|
2450
|
-
})
|
|
2451
|
-
);
|
|
2452
|
-
} else if (content.type === "card") {
|
|
2453
|
-
ret.push(await getCourseDB2(content.courseID).get(content.cardID));
|
|
2528
|
+
/**
|
|
2529
|
+
*
|
|
2530
|
+
* @returns A promise of the cards that the user has seen in the past.
|
|
2531
|
+
*/
|
|
2532
|
+
async getHistory() {
|
|
2533
|
+
const cards = await filterAllDocsByPrefix2(
|
|
2534
|
+
this.remoteDB,
|
|
2535
|
+
DocTypePrefixes["CARDRECORD" /* CARDRECORD */],
|
|
2536
|
+
{
|
|
2537
|
+
include_docs: true,
|
|
2538
|
+
attachments: false
|
|
2454
2539
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2540
|
+
);
|
|
2541
|
+
return cards.rows.map((r) => r.doc);
|
|
2542
|
+
}
|
|
2543
|
+
async updateCourseSettings(course_id, settings) {
|
|
2544
|
+
void this.getCourseRegistrationsDoc().then((doc) => {
|
|
2545
|
+
const crs = doc.courses.find((c) => c.courseID === course_id);
|
|
2546
|
+
if (crs) {
|
|
2547
|
+
if (crs.settings === null || crs.settings === void 0) {
|
|
2548
|
+
crs.settings = {};
|
|
2549
|
+
}
|
|
2550
|
+
settings.forEach((setting) => {
|
|
2551
|
+
crs.settings[setting.key] = setting.value;
|
|
2552
|
+
});
|
|
2462
2553
|
}
|
|
2554
|
+
return this.localDB.put(doc);
|
|
2463
2555
|
});
|
|
2464
2556
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2557
|
+
async getCourseSettings(course_id) {
|
|
2558
|
+
const regDoc = await this.getCourseRegistrationsDoc();
|
|
2559
|
+
const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
|
|
2560
|
+
if (crsDoc) {
|
|
2561
|
+
return crsDoc.settings;
|
|
2562
|
+
} else {
|
|
2563
|
+
throw new Error(`getCourseSettings Failed:
|
|
2564
|
+
User is not registered for course ${course_id}`);
|
|
2565
|
+
}
|
|
2471
2566
|
}
|
|
2472
|
-
async
|
|
2473
|
-
|
|
2474
|
-
const stuDbName = `classdb-student-${this._id}`;
|
|
2475
|
-
this._db = new pouchdb_setup_default(
|
|
2476
|
-
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2477
|
-
pouchDBincludeCredentialsConfig
|
|
2478
|
-
);
|
|
2479
|
-
this._stuDb = new pouchdb_setup_default(
|
|
2480
|
-
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
|
|
2481
|
-
pouchDBincludeCredentialsConfig
|
|
2482
|
-
);
|
|
2567
|
+
async getOrCreateClassroomRegistrationsDoc() {
|
|
2568
|
+
let ret;
|
|
2483
2569
|
try {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
}).then(() => {
|
|
2488
|
-
return;
|
|
2489
|
-
});
|
|
2570
|
+
ret = await this.remoteDB.get(
|
|
2571
|
+
_BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
|
|
2572
|
+
);
|
|
2490
2573
|
} catch (e) {
|
|
2491
|
-
|
|
2574
|
+
const err = e;
|
|
2575
|
+
if (err.status === 404) {
|
|
2576
|
+
await this.writeDB.put({
|
|
2577
|
+
_id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
|
|
2578
|
+
registrations: []
|
|
2579
|
+
});
|
|
2580
|
+
ret = await this.getOrCreateClassroomRegistrationsDoc();
|
|
2581
|
+
} else {
|
|
2582
|
+
const errorDetails = {
|
|
2583
|
+
name: err.name,
|
|
2584
|
+
status: err.status,
|
|
2585
|
+
message: err.message,
|
|
2586
|
+
reason: err.reason,
|
|
2587
|
+
error: err.error
|
|
2588
|
+
};
|
|
2589
|
+
logger.error(
|
|
2590
|
+
"Database error in getOrCreateClassroomRegistrationsDoc (private method):",
|
|
2591
|
+
errorDetails
|
|
2592
|
+
);
|
|
2593
|
+
throw new Error(
|
|
2594
|
+
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2492
2597
|
}
|
|
2493
|
-
|
|
2494
|
-
static async factory(classID) {
|
|
2495
|
-
const ret = new _TeacherClassroomDB(classID);
|
|
2496
|
-
await ret.init();
|
|
2598
|
+
logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
|
|
2497
2599
|
return ret;
|
|
2498
2600
|
}
|
|
2499
|
-
|
|
2500
|
-
|
|
2601
|
+
/**
|
|
2602
|
+
* Retrieves the list of active classroom IDs where the user is registered as a student.
|
|
2603
|
+
*
|
|
2604
|
+
* @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
|
|
2605
|
+
* registration document is unavailable due to database errors
|
|
2606
|
+
*
|
|
2607
|
+
* @description This method gracefully handles database connectivity issues by returning
|
|
2608
|
+
* an empty array when the classroom registrations document cannot be accessed.
|
|
2609
|
+
* This ensures that users can still access other application features even
|
|
2610
|
+
* when classroom functionality is temporarily unavailable.
|
|
2611
|
+
*/
|
|
2612
|
+
async getActiveClasses() {
|
|
2501
2613
|
try {
|
|
2502
|
-
|
|
2503
|
-
await this._db.remove(doc);
|
|
2504
|
-
void this._db.replicate.to(this._stuDb, {
|
|
2505
|
-
doc_ids: [contentID]
|
|
2506
|
-
});
|
|
2614
|
+
return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
|
|
2507
2615
|
} catch (error) {
|
|
2508
|
-
logger.
|
|
2616
|
+
logger.warn(
|
|
2617
|
+
"Failed to load classroom registrations, continuing without classroom data:",
|
|
2618
|
+
error
|
|
2619
|
+
);
|
|
2620
|
+
return [];
|
|
2509
2621
|
}
|
|
2510
2622
|
}
|
|
2511
|
-
async
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
_id: id,
|
|
2529
|
-
assignedBy: content.assignedBy,
|
|
2530
|
-
assignedOn: moment4.utc(),
|
|
2531
|
-
activeOn: content.activeOn || moment4.utc()
|
|
2532
|
-
});
|
|
2533
|
-
}
|
|
2534
|
-
if (put.ok) {
|
|
2535
|
-
void this._db.replicate.to(this._stuDb, {
|
|
2536
|
-
doc_ids: [id]
|
|
2537
|
-
});
|
|
2538
|
-
return true;
|
|
2539
|
-
} else {
|
|
2540
|
-
return false;
|
|
2541
|
-
}
|
|
2623
|
+
async scheduleCardReview(review) {
|
|
2624
|
+
return scheduleCardReviewLocal(this.writeDB, review);
|
|
2625
|
+
}
|
|
2626
|
+
async removeScheduledCardReview(reviewId) {
|
|
2627
|
+
return removeScheduledCardReviewLocal(this.writeDB, reviewId);
|
|
2628
|
+
}
|
|
2629
|
+
async registerForClassroom(_classId, _registerAs) {
|
|
2630
|
+
return registerUserForClassroom(this._username, _classId, _registerAs);
|
|
2631
|
+
}
|
|
2632
|
+
async dropFromClassroom(classId) {
|
|
2633
|
+
return dropUserFromClassroom(this._username, classId);
|
|
2634
|
+
}
|
|
2635
|
+
async getUserClassrooms() {
|
|
2636
|
+
return getUserClassrooms(this._username);
|
|
2637
|
+
}
|
|
2638
|
+
async updateUserElo(courseId, elo) {
|
|
2639
|
+
return updateUserElo(this._username, courseId, elo);
|
|
2542
2640
|
}
|
|
2543
2641
|
};
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
});
|
|
2642
|
+
userCoursesDoc = "CourseRegistrations";
|
|
2643
|
+
userClassroomsDoc = "ClassroomRegistrations";
|
|
2547
2644
|
}
|
|
2548
2645
|
});
|
|
2549
2646
|
|
|
2550
|
-
// src/
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
return await StudentClassroomDB.factory(source.id, user);
|
|
2558
|
-
} else {
|
|
2559
|
-
return getDataLayer().getCourseDB(source.id);
|
|
2647
|
+
// src/impl/common/index.ts
|
|
2648
|
+
var init_common = __esm({
|
|
2649
|
+
"src/impl/common/index.ts"() {
|
|
2650
|
+
"use strict";
|
|
2651
|
+
init_SyncStrategy();
|
|
2652
|
+
init_BaseUserDB();
|
|
2653
|
+
init_userDBHelpers();
|
|
2560
2654
|
}
|
|
2655
|
+
});
|
|
2656
|
+
|
|
2657
|
+
// src/factory.ts
|
|
2658
|
+
function getDataLayer() {
|
|
2659
|
+
if (!dataLayerInstance) {
|
|
2660
|
+
throw new Error("Data layer not initialized. Call initializeDataLayer first.");
|
|
2661
|
+
}
|
|
2662
|
+
return dataLayerInstance;
|
|
2561
2663
|
}
|
|
2562
|
-
var
|
|
2563
|
-
|
|
2664
|
+
var ENV, dataLayerInstance;
|
|
2665
|
+
var init_factory = __esm({
|
|
2666
|
+
"src/factory.ts"() {
|
|
2564
2667
|
"use strict";
|
|
2565
|
-
|
|
2566
|
-
|
|
2668
|
+
init_common();
|
|
2669
|
+
init_logger();
|
|
2670
|
+
ENV = {
|
|
2671
|
+
COUCHDB_SERVER_PROTOCOL: "NOT_SET",
|
|
2672
|
+
COUCHDB_SERVER_URL: "NOT_SET"
|
|
2673
|
+
};
|
|
2674
|
+
dataLayerInstance = null;
|
|
2567
2675
|
}
|
|
2568
2676
|
});
|
|
2569
2677
|
|
|
2570
2678
|
// src/impl/couch/adminDB.ts
|
|
2571
2679
|
var AdminDB;
|
|
2572
|
-
var
|
|
2680
|
+
var init_adminDB2 = __esm({
|
|
2573
2681
|
"src/impl/couch/adminDB.ts"() {
|
|
2574
2682
|
"use strict";
|
|
2575
2683
|
init_pouchdb_setup();
|
|
2576
2684
|
init_factory();
|
|
2577
2685
|
init_couch();
|
|
2578
|
-
|
|
2686
|
+
init_classroomDB2();
|
|
2579
2687
|
init_courseLookupDB();
|
|
2580
2688
|
init_logger();
|
|
2581
2689
|
AdminDB = class {
|
|
@@ -2589,7 +2697,7 @@ var init_adminDB = __esm({
|
|
|
2589
2697
|
async getUsers() {
|
|
2590
2698
|
return (await this.usersDB.allDocs({
|
|
2591
2699
|
include_docs: true,
|
|
2592
|
-
...
|
|
2700
|
+
...getStartAndEndKeys("org.couchdb.user:")
|
|
2593
2701
|
})).rows.map((r) => r.doc);
|
|
2594
2702
|
}
|
|
2595
2703
|
async getCourses() {
|
|
@@ -2682,7 +2790,7 @@ var init_auth = __esm({
|
|
|
2682
2790
|
});
|
|
2683
2791
|
|
|
2684
2792
|
// src/impl/couch/CouchDBSyncStrategy.ts
|
|
2685
|
-
import { Status as
|
|
2793
|
+
import { Status as Status4 } from "@vue-skuilder/common";
|
|
2686
2794
|
var log4, CouchDBSyncStrategy;
|
|
2687
2795
|
var init_CouchDBSyncStrategy = __esm({
|
|
2688
2796
|
"src/impl/couch/CouchDBSyncStrategy.ts"() {
|
|
@@ -2707,6 +2815,13 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2707
2815
|
return this.getUserDB(username);
|
|
2708
2816
|
}
|
|
2709
2817
|
}
|
|
2818
|
+
getWriteDB(username) {
|
|
2819
|
+
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
2820
|
+
return getLocalUserDB(username);
|
|
2821
|
+
} else {
|
|
2822
|
+
return this.getUserDB(username);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2710
2825
|
startSync(localDB, remoteDB) {
|
|
2711
2826
|
if (localDB !== remoteDB) {
|
|
2712
2827
|
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
@@ -2741,32 +2856,32 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
2741
2856
|
log4(`CREATEACCOUNT: logged in as new user: ${loginResult.ok}`);
|
|
2742
2857
|
if (loginResult.ok) {
|
|
2743
2858
|
return {
|
|
2744
|
-
status:
|
|
2859
|
+
status: Status4.ok,
|
|
2745
2860
|
error: void 0
|
|
2746
2861
|
};
|
|
2747
2862
|
} else {
|
|
2748
2863
|
return {
|
|
2749
|
-
status:
|
|
2864
|
+
status: Status4.error,
|
|
2750
2865
|
error: "Failed to log in after account creation"
|
|
2751
2866
|
};
|
|
2752
2867
|
}
|
|
2753
2868
|
} else {
|
|
2754
2869
|
logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
|
|
2755
2870
|
return {
|
|
2756
|
-
status:
|
|
2871
|
+
status: Status4.error,
|
|
2757
2872
|
error: "Account creation failed"
|
|
2758
2873
|
};
|
|
2759
2874
|
}
|
|
2760
2875
|
} catch (e) {
|
|
2761
2876
|
if (e.reason === "Document update conflict.") {
|
|
2762
2877
|
return {
|
|
2763
|
-
status:
|
|
2878
|
+
status: Status4.error,
|
|
2764
2879
|
error: "This username is taken!"
|
|
2765
2880
|
};
|
|
2766
2881
|
}
|
|
2767
2882
|
logger.error(`Error on signup: ${JSON.stringify(e)}`);
|
|
2768
2883
|
return {
|
|
2769
|
-
status:
|
|
2884
|
+
status: Status4.error,
|
|
2770
2885
|
error: e.message || "Unknown error during account creation"
|
|
2771
2886
|
};
|
|
2772
2887
|
}
|
|
@@ -2965,16 +3080,16 @@ function scheduleCardReview(review) {
|
|
|
2965
3080
|
const now = moment5.utc();
|
|
2966
3081
|
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
2967
3082
|
void getCouchUserDB(review.user).put({
|
|
2968
|
-
_id:
|
|
3083
|
+
_id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
|
|
2969
3084
|
cardId: review.card_id,
|
|
2970
|
-
reviewTime: review.time,
|
|
3085
|
+
reviewTime: review.time.toISOString(),
|
|
2971
3086
|
courseId: review.course_id,
|
|
2972
|
-
scheduledAt: now,
|
|
3087
|
+
scheduledAt: now.toISOString(),
|
|
2973
3088
|
scheduledFor: review.scheduledFor,
|
|
2974
3089
|
schedulingAgentId: review.schedulingAgentId
|
|
2975
3090
|
});
|
|
2976
3091
|
}
|
|
2977
|
-
function
|
|
3092
|
+
function filterAllDocsByPrefix(db, prefix, opts) {
|
|
2978
3093
|
const options = {
|
|
2979
3094
|
startkey: prefix,
|
|
2980
3095
|
endkey: prefix + "\uFFF0",
|
|
@@ -2985,13 +3100,13 @@ function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
|
2985
3100
|
}
|
|
2986
3101
|
return db.allDocs(options);
|
|
2987
3102
|
}
|
|
2988
|
-
function
|
|
3103
|
+
function getStartAndEndKeys(key) {
|
|
2989
3104
|
return {
|
|
2990
3105
|
startkey: key,
|
|
2991
3106
|
endkey: key + "\uFFF0"
|
|
2992
3107
|
};
|
|
2993
3108
|
}
|
|
2994
|
-
var isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig,
|
|
3109
|
+
var isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_TIME_FORMAT;
|
|
2995
3110
|
var init_couch = __esm({
|
|
2996
3111
|
"src/impl/couch/index.ts"() {
|
|
2997
3112
|
init_factory();
|
|
@@ -2999,8 +3114,8 @@ var init_couch = __esm({
|
|
|
2999
3114
|
init_logger();
|
|
3000
3115
|
init_pouchdb_setup();
|
|
3001
3116
|
init_contentSource();
|
|
3002
|
-
|
|
3003
|
-
|
|
3117
|
+
init_adminDB2();
|
|
3118
|
+
init_classroomDB2();
|
|
3004
3119
|
init_courseAPI();
|
|
3005
3120
|
init_courseDB();
|
|
3006
3121
|
init_CouchDBSyncStrategy();
|
|
@@ -3017,8 +3132,7 @@ var init_couch = __esm({
|
|
|
3017
3132
|
return pouchdb_setup_default.fetch(url, opts);
|
|
3018
3133
|
}
|
|
3019
3134
|
};
|
|
3020
|
-
|
|
3021
|
-
REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
3135
|
+
REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
3022
3136
|
}
|
|
3023
3137
|
});
|
|
3024
3138
|
init_couch();
|
|
@@ -3029,15 +3143,14 @@ export {
|
|
|
3029
3143
|
CouchDBSyncStrategy,
|
|
3030
3144
|
CourseDB,
|
|
3031
3145
|
CoursesDB,
|
|
3032
|
-
|
|
3033
|
-
REVIEW_TIME_FORMAT2 as REVIEW_TIME_FORMAT,
|
|
3146
|
+
REVIEW_TIME_FORMAT,
|
|
3034
3147
|
StudentClassroomDB,
|
|
3035
3148
|
TeacherClassroomDB,
|
|
3036
3149
|
addNote55,
|
|
3037
3150
|
addTagToCard,
|
|
3038
3151
|
createTag,
|
|
3039
3152
|
deleteTag,
|
|
3040
|
-
|
|
3153
|
+
filterAllDocsByPrefix,
|
|
3041
3154
|
getAncestorTagIDs,
|
|
3042
3155
|
getAppliedTags,
|
|
3043
3156
|
getChildTagStubs,
|
|
@@ -3054,7 +3167,7 @@ export {
|
|
|
3054
3167
|
getCredentialledDataShapes,
|
|
3055
3168
|
getLatestVersion,
|
|
3056
3169
|
getRandomCards,
|
|
3057
|
-
|
|
3170
|
+
getStartAndEndKeys,
|
|
3058
3171
|
getStudySource,
|
|
3059
3172
|
getTag,
|
|
3060
3173
|
getTagID,
|