MOBILE-2348 quiz: Fix problems in prefetch and offline

main
Dani Palou 2018-04-05 16:17:03 +02:00
parent 0b50cdfc21
commit ad1e564600
11 changed files with 51 additions and 23 deletions

View File

@ -16,6 +16,7 @@ import { Component, Optional, Injector } from '@angular/core';
import { Content, NavController } from 'ionic-angular'; import { Content, NavController } from 'ionic-angular';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { AddonModQuizProvider } from '../../providers/quiz'; import { AddonModQuizProvider } from '../../providers/quiz';
import { AddonModQuizHelperProvider } from '../../providers/helper'; import { AddonModQuizHelperProvider } from '../../providers/helper';
import { AddonModQuizOfflineProvider } from '../../providers/quiz-offline'; import { AddonModQuizOfflineProvider } from '../../providers/quiz-offline';
@ -70,7 +71,8 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() protected content: Content, constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() protected content: Content,
protected quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider, protected quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider,
protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate, protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate,
protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController) { protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController,
protected prefetchDelegate: CoreCourseModulePrefetchDelegate) {
super(injector); super(injector);
} }
@ -87,6 +89,8 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
this.quizProvider.logViewQuiz(this.quizData.id).then(() => { this.quizProvider.logViewQuiz(this.quizData.id).then(() => {
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
}).catch((error) => {
// Ignore errors.
}); });
}); });
@ -110,7 +114,10 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
if (this.quizProvider.isQuizOffline(this.quizData)) { if (this.quizProvider.isQuizOffline(this.quizData)) {
// Quiz supports offline, check if it needs to be downloaded. // Quiz supports offline, check if it needs to be downloaded.
if (this.currentStatus != CoreConstants.DOWNLOADED) { // If the site doesn't support check updates, always prefetch it because we cannot tell if there's something new.
const isDownloaded = this.currentStatus == CoreConstants.DOWNLOADED;
if (!isDownloaded || !this.prefetchDelegate.canCheckUpdates()) {
// Prefetch the quiz. // Prefetch the quiz.
this.showStatusSpinner = true; this.showStatusSpinner = true;
@ -118,8 +125,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
// Success downloading, open quiz. // Success downloading, open quiz.
this.openQuiz(); this.openQuiz();
}).catch((error) => { }).catch((error) => {
if (this.hasOffline) { if (this.hasOffline || (isDownloaded && !this.prefetchDelegate.canCheckUpdates())) {
// Error downloading but there is something offline, allow continuing it. // Error downloading but there is something offline, allow continuing it.
// If the site doesn't support check updates, continue too because we cannot tell if there's something new.
this.openQuiz(); this.openQuiz();
} else { } else {
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);

View File

@ -420,7 +420,9 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
}); });
// Mark the page as viewed. We'll ignore errors in this call. // Mark the page as viewed. We'll ignore errors in this call.
this.quizProvider.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline); this.quizProvider.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline).catch((error) => {
// Ignore errors.
});
// Start looking for changes. // Start looking for changes.
this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline); this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline);
@ -445,7 +447,9 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
this.attempt.dueDateWarning = this.quizProvider.getAttemptDueDateWarning(this.quiz, this.attempt); this.attempt.dueDateWarning = this.quizProvider.getAttemptDueDateWarning(this.quiz, this.attempt);
// Log summary as viewed. // Log summary as viewed.
this.quizProvider.logViewAttemptSummary(this.attempt.id, this.preflightData); this.quizProvider.logViewAttemptSummary(this.attempt.id, this.preflightData).catch((error) => {
// Ignore errors.
});
}); });
} }

View File

@ -77,7 +77,9 @@ export class AddonModQuizReviewPage implements OnInit {
*/ */
ngOnInit(): void { ngOnInit(): void {
this.fetchData().then(() => { this.fetchData().then(() => {
this.quizProvider.logViewAttemptReview(this.attemptId); this.quizProvider.logViewAttemptReview(this.attemptId).catch((error) => {
// Ignore errors.
});
}).finally(() => { }).finally(() => {
this.loaded = true; this.loaded = true;
}); });

View File

@ -78,7 +78,7 @@ export class AddonModQuizHelperProvider {
return Promise.reject(error); return Promise.reject(error);
} else if (retrying && !isPreflightCheckRequired) { } else if (retrying && !isPreflightCheckRequired) {
// We're retrying after a failure, but the preflight check wasn't required. // We're retrying after a failure, but the preflight check wasn't required.
// If this happens it means there's something wrong with some access rule. // This means there's something wrong with some access rule or user is offline and data isn't cached.
// Don't retry again because it would lead to an infinite loop. // Don't retry again because it would lead to an infinite loop.
return Promise.reject(error); return Promise.reject(error);
} else { } else {

View File

@ -233,7 +233,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseModulePrefetchHandler
return Promise.all(promises); return Promise.all(promises);
}).then(() => { }).then(() => {
// Check if we need to start a new attempt. // Check if we need to start a new attempt.
const attempt = attempts[attempts.length - 1]; let attempt = attempts[attempts.length - 1];
if (!attempt || this.quizProvider.isAttemptFinished(attempt.state)) { if (!attempt || this.quizProvider.isAttemptFinished(attempt.state)) {
// Check if the user can attempt the quiz. // Check if the user can attempt the quiz.
if (attemptAccessInfo.preventnewattemptreasons.length) { if (attemptAccessInfo.preventnewattemptreasons.length) {
@ -241,6 +241,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseModulePrefetchHandler
} }
startAttempt = true; startAttempt = true;
attempt = undefined;
} }
// Get the preflight data. This function will also start a new attempt if needed. // Get the preflight data. This function will also start a new attempt if needed.
@ -407,9 +408,11 @@ export class AddonModQuizPrefetchHandler extends CoreCourseModulePrefetchHandler
})); }));
promises.push(this.quizProvider.getGradeFromGradebook(quiz.course, quiz.coursemodule, true, siteId) promises.push(this.quizProvider.getGradeFromGradebook(quiz.course, quiz.coursemodule, true, siteId)
.then((gradebookData) => { .then((gradebookData) => {
if (typeof gradebookData.grade != 'undefined') { if (typeof gradebookData.graderaw != 'undefined') {
return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId); return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId);
} }
}).catch(() => {
// Ignore errors.
})); }));
promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt. promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt.

View File

@ -66,6 +66,10 @@ export class AddonModQuizOfflineProvider {
name: 'timecreated', name: 'timecreated',
type: 'INTEGER' type: 'INTEGER'
}, },
{
name: 'timemodified',
type: 'INTEGER'
},
{ {
name: 'finished', name: 'finished',
type: 'INTEGER' type: 'INTEGER'
@ -245,7 +249,7 @@ export class AddonModQuizOfflineProvider {
}).then((entry) => { }).then((entry) => {
// Save attempt in DB. // Save attempt in DB.
entry.timemodified = now; entry.timemodified = now;
entry.finished = !!finish; entry.finished = finish ? 1 : 0;
return db.insertRecord(this.ATTEMPTS_TABLE, entry); return db.insertRecord(this.ATTEMPTS_TABLE, entry);
}).then(() => { }).then(() => {

View File

@ -343,6 +343,10 @@ export class SQLiteDB {
* @param {object} data Data to insert. * @param {object} data Data to insert.
*/ */
protected formatDataToInsert(data: object): void { protected formatDataToInsert(data: object): void {
if (!data) {
return;
}
// Remove undefined entries and convert null to "NULL". // Remove undefined entries and convert null to "NULL".
for (const name in data) { for (const name in data) {
const value = data[name]; const value = data[name];
@ -782,6 +786,8 @@ export class SQLiteDB {
*/ */
updateRecords(table: string, data: any, conditions?: any): Promise<any> { updateRecords(table: string, data: any, conditions?: any): Promise<any> {
this.formatDataToInsert(data);
if (!data || !Object.keys(data).length) { if (!data || !Object.keys(data).length) {
// No fields to update, consider it's done. // No fields to update, consider it's done.
return Promise.resolve(); return Promise.resolve();
@ -792,8 +798,6 @@ export class SQLiteDB {
let sql, let sql,
params; params;
this.formatDataToInsert(data);
for (const key in data) { for (const key in data) {
sets.push(`${key} = ?`); sets.push(`${key} = ?`);
} }

View File

@ -205,6 +205,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
// Also, get the current status. // Also, get the current status.
this.modulePrefetchProvider.getModuleStatus(this.module, this.courseId).then((status) => { this.modulePrefetchProvider.getModuleStatus(this.module, this.courseId).then((status) => {
this.currentStatus = status;
this.showStatus(status); this.showStatus(status);
}); });
} }

View File

@ -66,6 +66,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
protected prefetchHandler: CoreCourseModulePrefetchHandler; protected prefetchHandler: CoreCourseModulePrefetchHandler;
protected statusObserver; protected statusObserver;
protected isDestroyed = false;
constructor(@Optional() protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate, constructor(@Optional() protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate,
protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider, protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider,
@ -128,14 +129,13 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
// Get download size to ask for confirm if it's high. // Get download size to ask for confirm if it's high.
this.prefetchHandler.getDownloadSize(module, this.courseId).then((size) => { this.prefetchHandler.getDownloadSize(module, this.courseId).then((size) => {
this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh).catch((error) => { return this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh);
// Error or cancelled.
this.spinner = false;
});
}).catch((error) => { }).catch((error) => {
// Error getting download size, hide spinner. // Error, hide spinner.
this.spinner = false; this.spinner = false;
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); if (!this.isDestroyed && error) {
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
}
}); });
} }
@ -158,5 +158,6 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.statusObserver && this.statusObserver.off(); this.statusObserver && this.statusObserver.off();
this.isDestroyed = true;
} }
} }

View File

@ -405,7 +405,8 @@ export class CoreQuestionProvider {
*/ */
getQuestionAnswers(component: string, attemptId: number, slot: number, filter?: boolean, siteId?: string): Promise<any[]> { getQuestionAnswers(component: string, attemptId: number, slot: number, filter?: boolean, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, slot}).then((answers) => { return site.getDb().getRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, questionSlot: slot})
.then((answers) => {
if (filter) { if (filter) {
// Get only answers that isn't "extra" data like sequencecheck or certainty. // Get only answers that isn't "extra" data like sequencecheck or certainty.
return this.getBasicAnswersFromArray(answers); return this.getBasicAnswersFromArray(answers);
@ -525,7 +526,7 @@ export class CoreQuestionProvider {
*/ */
removeQuestionAnswers(component: string, attemptId: number, slot: number, siteId?: string): Promise<any> { removeQuestionAnswers(component: string, attemptId: number, slot: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, slot}); return site.getDb().deleteRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, questionSlot: slot});
}); });
} }

View File

@ -653,10 +653,10 @@ export class CoreFilepoolProvider {
if (filePath && entry.path !== filePath) { if (filePath && entry.path !== filePath) {
newData.path = filePath; newData.path = filePath;
} }
if (entry.isexternalfile !== options.isexternalfile) { if (entry.isexternalfile !== options.isexternalfile && (entry.isexternalfile || options.isexternalfile)) {
newData.isexternalfile = options.isexternalfile; newData.isexternalfile = options.isexternalfile;
} }
if (entry.repositorytype !== options.repositorytype) { if (entry.repositorytype !== options.repositorytype && (entry.repositorytype || options.repositorytype)) {
newData.repositorytype = options.repositorytype; newData.repositorytype = options.repositorytype;
} }
@ -2659,7 +2659,7 @@ export class CoreFilepoolProvider {
// Going back from downloading to previous status, restore previous download time. // Going back from downloading to previous status, restore previous download time.
newData.downloadTime = entry.previousDownloadTime; newData.downloadTime = entry.previousDownloadTime;
} }
newData.status = entry.previous || CoreConstants.DOWNLOADED; newData.status = entry.previous || CoreConstants.NOT_DOWNLOADED;
newData.updated = Date.now(); newData.updated = Date.now();
this.logger.debug(`Set previous status '${entry.status}' for package ${component} ${componentId}`); this.logger.debug(`Set previous status '${entry.status}' for package ${component} ${componentId}`);