forked from EVOgeek/Vmeda.Online
373 lines
14 KiB
TypeScript
373 lines
14 KiB
TypeScript
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
import { CoreQuestionBehaviourDelegate, CoreQuestionQuestionWithAnswers } from '@features/question/services/behaviour-delegate';
|
|
import { CoreQuestionAnswerDBRecord } from '@features/question/services/database/question';
|
|
import { CoreQuestion, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
|
|
import { CoreSites } from '@services/sites';
|
|
import { CoreTimeUtils } from '@services/utils/time';
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
import { makeSingleton, Translate } from '@singletons';
|
|
import { CoreLogger } from '@singletons/logger';
|
|
import { AddonModQuizAttemptDBRecord, ATTEMPTS_TABLE_NAME } from './database/quiz';
|
|
import { AddonModQuizAttemptWSData, AddonModQuizProvider, AddonModQuizQuizWSData } from './quiz';
|
|
|
|
/**
|
|
* Service to handle offline quiz.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class AddonModQuizOfflineProvider {
|
|
|
|
protected logger: CoreLogger;
|
|
|
|
constructor() {
|
|
this.logger = CoreLogger.getInstance('AddonModQuizOfflineProvider');
|
|
}
|
|
|
|
/**
|
|
* Classify the answers in questions.
|
|
*
|
|
* @param answers List of answers.
|
|
* @return Object with the questions, the keys are the slot. Each question contains its answers.
|
|
*/
|
|
classifyAnswersInQuestions(answers: CoreQuestionsAnswers): AddonModQuizQuestionsWithAnswers {
|
|
const questionsWithAnswers: AddonModQuizQuestionsWithAnswers = {};
|
|
|
|
// Classify the answers in each question.
|
|
for (const name in answers) {
|
|
const slot = CoreQuestion.instance.getQuestionSlotFromName(name);
|
|
const nameWithoutPrefix = CoreQuestion.instance.removeQuestionPrefix(name);
|
|
|
|
if (!questionsWithAnswers[slot]) {
|
|
questionsWithAnswers[slot] = {
|
|
answers: {},
|
|
prefix: name.substr(0, name.indexOf(nameWithoutPrefix)),
|
|
};
|
|
}
|
|
questionsWithAnswers[slot].answers[nameWithoutPrefix] = answers[name];
|
|
}
|
|
|
|
return questionsWithAnswers;
|
|
}
|
|
|
|
/**
|
|
* Given a list of questions with answers classified in it, returns a list of answers (including prefix in the name).
|
|
*
|
|
* @param questions Questions.
|
|
* @return Answers.
|
|
*/
|
|
extractAnswersFromQuestions(questions: AddonModQuizQuestionsWithAnswers): CoreQuestionsAnswers {
|
|
const answers: CoreQuestionsAnswers = {};
|
|
|
|
for (const slot in questions) {
|
|
const question = questions[slot];
|
|
|
|
for (const name in question.answers) {
|
|
answers[question.prefix + name] = question.answers[name];
|
|
}
|
|
}
|
|
|
|
return answers;
|
|
}
|
|
|
|
/**
|
|
* Get all the offline attempts in a certain site.
|
|
*
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with the offline attempts.
|
|
*/
|
|
async getAllAttempts(siteId?: string): Promise<AddonModQuizAttemptDBRecord[]> {
|
|
const db = await CoreSites.instance.getSiteDb(siteId);
|
|
|
|
return db.getAllRecords(ATTEMPTS_TABLE_NAME);
|
|
}
|
|
|
|
/**
|
|
* Retrieve an attempt answers from site DB.
|
|
*
|
|
* @param attemptId Attempt ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with the answers.
|
|
*/
|
|
getAttemptAnswers(attemptId: number, siteId?: string): Promise<CoreQuestionAnswerDBRecord[]> {
|
|
return CoreQuestion.instance.getAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId);
|
|
}
|
|
|
|
/**
|
|
* Retrieve an attempt from site DB.
|
|
*
|
|
* @param attemptId Attempt ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with the attempt.
|
|
*/
|
|
async getAttemptById(attemptId: number, siteId?: string): Promise<AddonModQuizAttemptDBRecord> {
|
|
const db = await CoreSites.instance.getSiteDb(siteId);
|
|
|
|
return db.getRecord(ATTEMPTS_TABLE_NAME, { id: attemptId });
|
|
}
|
|
|
|
/**
|
|
* Retrieve an attempt from site DB.
|
|
*
|
|
* @param attemptId Attempt ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @param userId User ID. If not defined, user current site's user.
|
|
* @return Promise resolved with the attempts.
|
|
*/
|
|
async getQuizAttempts(quizId: number, siteId?: string, userId?: number): Promise<AddonModQuizAttemptDBRecord[]> {
|
|
const site = await CoreSites.instance.getSite(siteId);
|
|
|
|
return site.getDb().getRecords(ATTEMPTS_TABLE_NAME, { quizid: quizId, userid: userId || site.getUserId() });
|
|
}
|
|
|
|
/**
|
|
* Load local state in the questions.
|
|
*
|
|
* @param attemptId Attempt ID.
|
|
* @param questions List of questions.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
async loadQuestionsLocalStates(
|
|
attemptId: number,
|
|
questions: CoreQuestionQuestionParsed[],
|
|
siteId?: string,
|
|
): Promise<CoreQuestionQuestionParsed[]> {
|
|
|
|
await Promise.all(questions.map(async (question) => {
|
|
const dbQuestion = await CoreUtils.instance.ignoreErrors(
|
|
CoreQuestion.instance.getQuestion(AddonModQuizProvider.COMPONENT, attemptId, question.slot, siteId),
|
|
);
|
|
|
|
if (!dbQuestion) {
|
|
// Question not found.
|
|
return;
|
|
}
|
|
|
|
const state = CoreQuestion.instance.getState(dbQuestion.state);
|
|
question.state = dbQuestion.state;
|
|
question.status = Translate.instance.instant('core.question.' + state.status);
|
|
}));
|
|
|
|
return questions;
|
|
}
|
|
|
|
/**
|
|
* Process an attempt, saving its data.
|
|
*
|
|
* @param quiz Quiz.
|
|
* @param attempt Attempt.
|
|
* @param questions Object with the questions of the quiz. The keys should be the question slot.
|
|
* @param data Data to save.
|
|
* @param finish Whether to finish the quiz.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved in success, rejected otherwise.
|
|
*/
|
|
async processAttempt(
|
|
quiz: AddonModQuizQuizWSData,
|
|
attempt: AddonModQuizAttemptWSData,
|
|
questions: Record<number, CoreQuestionQuestionParsed>,
|
|
data: CoreQuestionsAnswers,
|
|
finish?: boolean,
|
|
siteId?: string,
|
|
): Promise<void> {
|
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
|
const now = CoreTimeUtils.instance.timestamp();
|
|
|
|
const db = await CoreSites.instance.getSiteDb(siteId);
|
|
|
|
// Check if an attempt already exists. Return a new one if it doesn't.
|
|
let entry = await CoreUtils.instance.ignoreErrors(this.getAttemptById(attempt.id, siteId));
|
|
|
|
if (entry) {
|
|
entry.timemodified = now;
|
|
entry.finished = finish ? 1 : 0;
|
|
} else {
|
|
entry = {
|
|
quizid: quiz.id,
|
|
userid: attempt.userid!,
|
|
id: attempt.id,
|
|
courseid: quiz.course,
|
|
timecreated: now,
|
|
attempt: attempt.attempt!,
|
|
currentpage: attempt.currentpage,
|
|
timemodified: now,
|
|
finished: finish ? 1 : 0,
|
|
};
|
|
}
|
|
|
|
// Save attempt in DB.
|
|
await db.insertRecord(ATTEMPTS_TABLE_NAME, entry);
|
|
|
|
// Attempt has been saved, now we need to save the answers.
|
|
await this.saveAnswers(quiz, attempt, questions, data, now, siteId);
|
|
}
|
|
|
|
/**
|
|
* Remove an attempt and its answers from local DB.
|
|
*
|
|
* @param attemptId Attempt ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
async removeAttemptAndAnswers(attemptId: number, siteId?: string): Promise<void> {
|
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
|
|
|
const db = await CoreSites.instance.getSiteDb(siteId);
|
|
|
|
await Promise.all([
|
|
CoreQuestion.instance.removeAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId),
|
|
CoreQuestion.instance.removeAttemptQuestions(AddonModQuizProvider.COMPONENT, attemptId, siteId),
|
|
db.deleteRecords(ATTEMPTS_TABLE_NAME, { id: attemptId }),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Remove a question and its answers from local DB.
|
|
*
|
|
* @param attemptId Attempt ID.
|
|
* @param slot Question slot.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when finished.
|
|
*/
|
|
async removeQuestionAndAnswers(attemptId: number, slot: number, siteId?: string): Promise<void> {
|
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
|
|
|
await Promise.all([
|
|
CoreQuestion.instance.removeQuestion(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId),
|
|
CoreQuestion.instance.removeQuestionAnswers(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Save an attempt's answers and calculate state for questions modified.
|
|
*
|
|
* @param quiz Quiz.
|
|
* @param attempt Attempt.
|
|
* @param questions Object with the questions of the quiz. The keys should be the question slot.
|
|
* @param answers Answers to save.
|
|
* @param timeMod Time modified to set in the answers. If not defined, current time.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
async saveAnswers(
|
|
quiz: AddonModQuizQuizWSData,
|
|
attempt: AddonModQuizAttemptWSData,
|
|
questions: Record<number, CoreQuestionQuestionParsed>,
|
|
answers: CoreQuestionsAnswers,
|
|
timeMod?: number,
|
|
siteId?: string,
|
|
): Promise<void> {
|
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
|
timeMod = timeMod || CoreTimeUtils.instance.timestamp();
|
|
|
|
const questionsWithAnswers: Record<number, CoreQuestionQuestionWithAnswers> = {};
|
|
const newStates: Record<number, string> = {};
|
|
|
|
// Classify the answers in each question.
|
|
for (const name in answers) {
|
|
const slot = CoreQuestion.instance.getQuestionSlotFromName(name);
|
|
const nameWithoutPrefix = CoreQuestion.instance.removeQuestionPrefix(name);
|
|
|
|
if (questions[slot]) {
|
|
if (!questionsWithAnswers[slot]) {
|
|
questionsWithAnswers[slot] = questions[slot];
|
|
questionsWithAnswers[slot].answers = {};
|
|
}
|
|
questionsWithAnswers[slot].answers![nameWithoutPrefix] = answers[name];
|
|
}
|
|
}
|
|
|
|
// First determine the new state of each question. We won't save the new state yet.
|
|
await Promise.all(Object.values(questionsWithAnswers).map(async (question) => {
|
|
|
|
const state = await CoreQuestionBehaviourDelegate.instance.determineNewState(
|
|
quiz.preferredbehaviour!,
|
|
AddonModQuizProvider.COMPONENT,
|
|
attempt.id,
|
|
question,
|
|
quiz.coursemodule,
|
|
siteId,
|
|
);
|
|
|
|
// Check if state has changed.
|
|
if (state && state.name != question.state) {
|
|
newStates[question.slot] = state.name;
|
|
}
|
|
|
|
// Delete previously stored answers for this question.
|
|
await CoreQuestion.instance.removeQuestionAnswers(AddonModQuizProvider.COMPONENT, attempt.id, question.slot, siteId);
|
|
}));
|
|
|
|
// Now save the answers.
|
|
await CoreQuestion.instance.saveAnswers(
|
|
AddonModQuizProvider.COMPONENT,
|
|
quiz.id,
|
|
attempt.id,
|
|
attempt.userid!,
|
|
answers,
|
|
timeMod,
|
|
siteId,
|
|
);
|
|
|
|
try {
|
|
// Answers have been saved, now we can save the questions with the states.
|
|
await CoreUtils.instance.allPromises(Object.keys(newStates).map(async (slot) => {
|
|
const question = questionsWithAnswers[Number(slot)];
|
|
|
|
await CoreQuestion.instance.saveQuestion(
|
|
AddonModQuizProvider.COMPONENT,
|
|
quiz.id,
|
|
attempt.id,
|
|
attempt.userid!,
|
|
question,
|
|
newStates[slot],
|
|
siteId,
|
|
);
|
|
}));
|
|
} catch (error) {
|
|
// Ignore errors when saving question state.
|
|
this.logger.error('Error saving question state', error);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Set attempt's current page.
|
|
*
|
|
* @param attemptId Attempt ID.
|
|
* @param page Page to set.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved in success, rejected otherwise.
|
|
*/
|
|
async setAttemptCurrentPage(attemptId: number, page: number, siteId?: string): Promise<void> {
|
|
const db = await CoreSites.instance.getSiteDb(siteId);
|
|
|
|
await db.updateRecords(ATTEMPTS_TABLE_NAME, { currentpage: page }, { id: attemptId });
|
|
}
|
|
|
|
}
|
|
|
|
export class AddonModQuizOffline extends makeSingleton(AddonModQuizOfflineProvider) {}
|
|
|
|
/**
|
|
* Answers classified by question slot.
|
|
*/
|
|
export type AddonModQuizQuestionsWithAnswers = Record<number, {
|
|
prefix: string;
|
|
answers: CoreQuestionsAnswers;
|
|
}>;
|