extended admin tool for finished lessons
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
This commit is contained in:
@@ -34,6 +34,7 @@ class AdminController {
|
||||
this.getUsers = this.getUsers.bind(this);
|
||||
this.updateUser = this.updateUser.bind(this);
|
||||
this.resetUserVocabLessonProgress = this.resetUserVocabLessonProgress.bind(this);
|
||||
this.markUserVocabLessonsCompleteThrough = this.markUserVocabLessonsCompleteThrough.bind(this);
|
||||
this.getUserVocabCourses = this.getUserVocabCourses.bind(this);
|
||||
this.getVocabCourseForAdmin = this.getVocabCourseForAdmin.bind(this);
|
||||
this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this);
|
||||
@@ -151,6 +152,34 @@ class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
async markUserVocabLessonsCompleteThrough(req, res) {
|
||||
const schema = Joi.object({
|
||||
courseId: Joi.number().integer().positive().required(),
|
||||
throughLessonNumber: Joi.number().integer().positive().required()
|
||||
});
|
||||
const { error, value } = schema.validate(req.body || {});
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
const { id } = req.params;
|
||||
const result = await AdminService.adminMarkUserVocabLessonsCompleteThrough(
|
||||
requester,
|
||||
id,
|
||||
value.courseId,
|
||||
value.throughLessonNumber
|
||||
);
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
let status = 500;
|
||||
if (err.message === 'noaccess') status = 403;
|
||||
else if (err.message === 'notenrolled') status = 403;
|
||||
else if (err.message === 'badrequest') status = 400;
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getUserVocabCourses(req, res) {
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
|
||||
@@ -27,6 +27,11 @@ router.get('/users/erotic-moderation/preview/:type/:targetId', authenticate, adm
|
||||
router.put('/users/erotic-moderation/:id', authenticate, adminController.applyEroticModerationAction);
|
||||
router.get('/users/:id/vocab-courses', authenticate, adminController.getUserVocabCourses);
|
||||
router.post('/users/:id/vocab-lesson-progress/reset', authenticate, adminController.resetUserVocabLessonProgress);
|
||||
router.post(
|
||||
'/users/:id/vocab-lesson-progress/mark-complete-through',
|
||||
authenticate,
|
||||
adminController.markUserVocabLessonsCompleteThrough
|
||||
);
|
||||
router.get('/vocab/courses/:courseId', authenticate, adminController.getVocabCourseForAdmin);
|
||||
router.get('/users/:id', authenticate, adminController.getUser);
|
||||
router.put('/users/:id', authenticate, adminController.updateUser);
|
||||
|
||||
@@ -1974,6 +1974,28 @@ class AdminService {
|
||||
}
|
||||
}
|
||||
|
||||
async adminMarkUserVocabLessonsCompleteThrough(requesterHashedId, targetHashedId, courseId, throughLessonNumber) {
|
||||
if (!(await this.hasUserAccess(requesterHashedId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const vocab = new VocabService();
|
||||
try {
|
||||
return await vocab.adminMarkLessonsCompleteThrough(
|
||||
targetHashedId,
|
||||
Number(courseId),
|
||||
Number(throughLessonNumber)
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.status === 403) {
|
||||
throw new Error('notenrolled');
|
||||
}
|
||||
if (e.status === 400) {
|
||||
throw new Error('badrequest');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async adminListUserEnrolledVocabCourses(requesterHashedId, targetHashedId) {
|
||||
if (!(await this.hasUserAccess(requesterHashedId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
|
||||
@@ -2609,6 +2609,101 @@ export default class VocabService {
|
||||
return this._purgeLessonProgressForUser(user.id, lesson.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: Alle Lektionen eines Kurses bis einschließlich lesson_number als abgeschlossen setzen
|
||||
* (nur Zeilen, die noch nicht completed sind). Nur bei eingeschriebenem Nutzer.
|
||||
*/
|
||||
async adminMarkLessonsCompleteThrough(targetHashedUserId, courseId, throughLessonNumber) {
|
||||
const user = await this._getUserByHashedId(targetHashedUserId);
|
||||
const cid = Number(courseId);
|
||||
const maxNum = Number(throughLessonNumber);
|
||||
if (!Number.isFinite(cid) || cid < 1) {
|
||||
const err = new Error('Invalid courseId');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
if (!Number.isFinite(maxNum) || maxNum < 1) {
|
||||
const err = new Error('Invalid throughLessonNumber');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const enrollment = await VocabCourseEnrollment.findOne({
|
||||
where: { userId: user.id, courseId: cid }
|
||||
});
|
||||
if (!enrollment) {
|
||||
const err = new Error('Not enrolled in this course');
|
||||
err.status = 403;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: { courseId: cid, lessonNumber: { [Op.lte]: maxNum } },
|
||||
order: [['lessonNumber', 'ASC']]
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const details = [];
|
||||
|
||||
for (const lesson of lessons) {
|
||||
const lessonData = lesson.get({ plain: true });
|
||||
const targetScore = lessonData.targetScorePercent || 80;
|
||||
|
||||
const [progress] = await VocabCourseProgress.findOrCreate({
|
||||
where: { userId: user.id, lessonId: lesson.id },
|
||||
defaults: {
|
||||
userId: user.id,
|
||||
courseId: cid,
|
||||
lessonId: lesson.id,
|
||||
completed: false,
|
||||
score: 0,
|
||||
lessonState: {}
|
||||
}
|
||||
});
|
||||
|
||||
if (progress.completed) {
|
||||
details.push({
|
||||
lessonNumber: lesson.lessonNumber,
|
||||
lessonId: lesson.id,
|
||||
status: 'unchanged'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const mergedState = this._applyScheduledReviewState(
|
||||
this._sanitizeLessonState(progress.lessonState),
|
||||
{
|
||||
previousCompleted: false,
|
||||
nextCompleted: true,
|
||||
shouldAdvanceReview: true,
|
||||
lessonData,
|
||||
now
|
||||
}
|
||||
);
|
||||
|
||||
await progress.update({
|
||||
completed: true,
|
||||
completedAt: now,
|
||||
score: Math.max(Number(progress.score) || 0, targetScore),
|
||||
lastAccessedAt: now,
|
||||
lessonState: mergedState
|
||||
});
|
||||
|
||||
details.push({
|
||||
lessonNumber: lesson.lessonNumber,
|
||||
lessonId: lesson.id,
|
||||
status: 'marked_complete'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
courseId: cid,
|
||||
throughLessonNumber: maxNum,
|
||||
lessonsConsidered: lessons.length,
|
||||
details
|
||||
};
|
||||
}
|
||||
|
||||
// ========== GRAMMAR EXERCISE METHODS ==========
|
||||
|
||||
async getExerciseTypes() {
|
||||
|
||||
Reference in New Issue
Block a user