MOBILE-3659 course: Implement course sync
parent
9a935a3946
commit
310ee19d26
|
@ -0,0 +1,26 @@
|
||||||
|
// (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 { Translate } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network error. It will automatically set the right error message if none is provided.
|
||||||
|
*/
|
||||||
|
export class CoreNetworkError extends Error {
|
||||||
|
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message || Translate.instance.instant('core.networkerrormsg'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,6 +25,9 @@ import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log';
|
||||||
import { SITE_SCHEMA as PREFETCH_SITE_SCHEMA } from './services/database/module-prefetch';
|
import { SITE_SCHEMA as PREFETCH_SITE_SCHEMA } from './services/database/module-prefetch';
|
||||||
import { CoreCourseIndexRoutingModule } from './pages/index/index-routing.module';
|
import { CoreCourseIndexRoutingModule } from './pages/index/index-routing.module';
|
||||||
import { CoreCourseModulePrefetchDelegate } from './services/module-prefetch-delegate';
|
import { CoreCourseModulePrefetchDelegate } from './services/module-prefetch-delegate';
|
||||||
|
import { CoreCronDelegate } from '@services/cron';
|
||||||
|
import { CoreCourseLogCronHandler } from './services/handlers/log-cron';
|
||||||
|
import { CoreCourseSyncCronHandler } from './services/handlers/sync-cron';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -60,6 +63,9 @@ const courseIndexRoutes: Routes = [
|
||||||
multi: true,
|
multi: true,
|
||||||
deps: [],
|
deps: [],
|
||||||
useFactory: () => () => {
|
useFactory: () => () => {
|
||||||
|
CoreCronDelegate.instance.register(CoreCourseSyncCronHandler.instance);
|
||||||
|
CoreCronDelegate.instance.register(CoreCourseLogCronHandler.instance);
|
||||||
|
|
||||||
CoreCourseModulePrefetchDelegate.instance.initialize();
|
CoreCourseModulePrefetchDelegate.instance.initialize();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -36,7 +36,7 @@ import {
|
||||||
CoreCourseOptionsDelegate,
|
CoreCourseOptionsDelegate,
|
||||||
CoreCourseOptionsMenuHandlerToDisplay,
|
CoreCourseOptionsMenuHandlerToDisplay,
|
||||||
} from '@features/course/services/course-options-delegate';
|
} from '@features/course/services/course-options-delegate';
|
||||||
// import { CoreCourseSyncProvider } from '../../providers/sync';
|
import { CoreCourseAutoSyncData, CoreCourseSync, CoreCourseSyncProvider } from '@features/course/services/sync';
|
||||||
import { CoreCourseFormatComponent } from '../../components/format/format';
|
import { CoreCourseFormatComponent } from '../../components/format/format';
|
||||||
import {
|
import {
|
||||||
CoreEvents,
|
CoreEvents,
|
||||||
|
@ -144,15 +144,17 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// @todo this.syncObserver = CoreEvents.on(CoreCourseSyncProvider.AUTO_SYNCED, (data) => {
|
this.syncObserver = CoreEvents.on<CoreCourseAutoSyncData>(CoreCourseSyncProvider.AUTO_SYNCED, (data) => {
|
||||||
// if (data && data.courseId == this.course.id) {
|
if (!data || data.courseId != this.course.id) {
|
||||||
// this.refreshAfterCompletionChange(false);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// if (data.warnings && data.warnings[0]) {
|
this.refreshAfterCompletionChange(false);
|
||||||
// CoreDomUtils.instance.showErrorModal(data.warnings[0]);
|
|
||||||
// }
|
if (data.warnings && data.warnings[0]) {
|
||||||
// }
|
CoreDomUtils.instance.showErrorModal(data.warnings[0]);
|
||||||
// });
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -210,13 +212,11 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
if (sync) {
|
if (sync) {
|
||||||
// Try to synchronize the course data.
|
// Try to synchronize the course data.
|
||||||
// @todo return this.syncProvider.syncCourse(this.course.id).then((result) => {
|
// For now we don't allow manual syncing, so ignore errors.
|
||||||
// if (result.warnings && result.warnings.length) {
|
const result = await CoreUtils.instance.ignoreErrors(CoreCourseSync.instance.syncCourse(this.course.id));
|
||||||
// CoreDomUtils.instance.showErrorModal(result.warnings[0]);
|
if (result?.warnings?.length) {
|
||||||
// }
|
CoreDomUtils.instance.showErrorModal(result.warnings[0]);
|
||||||
// }).catch(() => {
|
}
|
||||||
// // For now we don't allow manual syncing, so ignore errors.
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -63,6 +63,7 @@ import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
import { CoreEventObserver, CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events';
|
||||||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefetch info of a module.
|
* Prefetch info of a module.
|
||||||
|
@ -732,7 +733,7 @@ export class CoreCourseHelperProvider {
|
||||||
try {
|
try {
|
||||||
path = await CoreFilepool.instance.getInternalUrlByUrl(site.getId(), fileUrl);
|
path = await CoreFilepool.instance.getInternalUrlByUrl(site.getId(), fileUrl);
|
||||||
} catch {
|
} catch {
|
||||||
throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CoreUtils.instance.openFile(path);
|
return CoreUtils.instance.openFile(path);
|
||||||
|
@ -857,7 +858,7 @@ export class CoreCourseHelperProvider {
|
||||||
|
|
||||||
if (!isOnline && status === CoreConstants.NOT_DOWNLOADED) {
|
if (!isOnline && status === CoreConstants.NOT_DOWNLOADED) {
|
||||||
// Not downloaded and we're offline, reject.
|
// Not downloaded and we're offline, reject.
|
||||||
throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldDownloadFirst = await CoreFilepool.instance.shouldDownloadFileBeforeOpen(fixedUrl, mainFile.filesize);
|
const shouldDownloadFirst = await CoreFilepool.instance.shouldDownloadFileBeforeOpen(fixedUrl, mainFile.filesize);
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
// (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 { CoreCronHandler } from '@services/cron';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log cron handler. It will update last access of the user while app is open.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreCourseLogCronHandlerService implements CoreCronHandler {
|
||||||
|
|
||||||
|
name = 'CoreCourseLogCronHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the process.
|
||||||
|
* Receives the ID of the site affected, undefined for all sites.
|
||||||
|
*
|
||||||
|
* @param siteId ID of the site affected, undefined for all sites.
|
||||||
|
* @param force Wether the execution is forced (manual sync).
|
||||||
|
* @return Promise resolved when done, rejected if failure.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async execute(siteId?: string, force?: boolean): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return CoreCourse.instance.logView(site.getSiteHomeId(), undefined, site.getId(), site.getInfo()?.sitename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether it's a synchronization process or not.
|
||||||
|
*
|
||||||
|
* @return Whether it's a synchronization process or not.
|
||||||
|
*/
|
||||||
|
isSync(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time between consecutive executions.
|
||||||
|
*
|
||||||
|
* @return Time between consecutive executions (in ms).
|
||||||
|
*/
|
||||||
|
getInterval(): number {
|
||||||
|
return 240000; // 4 minutes. By default platform will see the user as online if lastaccess is less than 5 minutes.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreCourseLogCronHandler extends makeSingleton(CoreCourseLogCronHandlerService) {}
|
|
@ -0,0 +1,52 @@
|
||||||
|
// (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 { CoreCronHandler } from '@services/cron';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreCourseSync } from '../sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization cron handler.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreCourseSyncCronHandlerService implements CoreCronHandler {
|
||||||
|
|
||||||
|
name = 'CoreCourseSyncCronHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the process.
|
||||||
|
* Receives the ID of the site affected, undefined for all sites.
|
||||||
|
*
|
||||||
|
* @param siteId ID of the site affected, undefined for all sites.
|
||||||
|
* @param force Wether the execution is forced (manual sync).
|
||||||
|
* @return Promise resolved when done, rejected if failure.
|
||||||
|
*/
|
||||||
|
execute(siteId?: string, force?: boolean): Promise<void> {
|
||||||
|
return CoreCourseSync.instance.syncAllCourses(siteId, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time between consecutive executions.
|
||||||
|
*
|
||||||
|
* @return Time between consecutive executions (in ms).
|
||||||
|
*/
|
||||||
|
getInterval(): number {
|
||||||
|
return CoreCourseSync.instance.syncInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreCourseSyncCronHandler extends makeSingleton(CoreCourseSyncCronHandlerService) {}
|
|
@ -0,0 +1,260 @@
|
||||||
|
// (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 { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||||
|
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreCourseOffline } from './course-offline';
|
||||||
|
import { CoreCourse } from './course';
|
||||||
|
import { CoreCourseLogHelper } from './log-helper';
|
||||||
|
import { CoreWSExternalWarning } from '@services/ws';
|
||||||
|
import { CoreCourseManualCompletionDBRecord } from './database/course';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to sync course offline data. This only syncs the offline data of the course itself, not the offline data of
|
||||||
|
* the activities in the course.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreCourseSyncProvider extends CoreSyncBaseProvider<CoreCourseSyncResult> {
|
||||||
|
|
||||||
|
static readonly AUTO_SYNCED = 'core_course_autom_synced';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('CoreCourseSyncProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to synchronize all the courses in a certain site or in all sites.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @param force Wether the execution is forced (manual sync).
|
||||||
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
syncAllCourses(siteId?: string, force?: boolean): Promise<void> {
|
||||||
|
return this.syncOnSites('courses', this.syncAllCoursesFunc.bind(this, siteId, force), siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync all courses on a site.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID to sync.
|
||||||
|
* @param force Wether the execution is forced (manual sync).
|
||||||
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
protected async syncAllCoursesFunc(siteId: string, force: boolean): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
CoreCourseLogHelper.instance.syncSite(siteId),
|
||||||
|
this.syncCoursesCompletion(siteId, force),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync courses offline completion.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID to sync.
|
||||||
|
* @param force Wether the execution is forced (manual sync).
|
||||||
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
protected async syncCoursesCompletion(siteId: string, force: boolean): Promise<void> {
|
||||||
|
const completions = await CoreCourseOffline.instance.getAllManualCompletions(siteId);
|
||||||
|
|
||||||
|
// Sync all courses.
|
||||||
|
await Promise.all(completions.map(async (completion) => {
|
||||||
|
const result = await (force ? this.syncCourse(completion.courseid, siteId) :
|
||||||
|
this.syncCourseIfNeeded(completion.courseid, siteId));
|
||||||
|
|
||||||
|
if (!result || !result.updated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync successful, send event.
|
||||||
|
CoreEvents.trigger<CoreCourseAutoSyncData>(CoreCourseSyncProvider.AUTO_SYNCED, {
|
||||||
|
courseId: completion.courseid,
|
||||||
|
warnings: result.warnings,
|
||||||
|
}, siteId);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync a course if it's needed.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID to be synced.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the course is synced or it doesn't need to be synced.
|
||||||
|
*/
|
||||||
|
syncCourseIfNeeded(courseId: number, siteId?: string): Promise<CoreCourseSyncResult> {
|
||||||
|
// Usually we call isSyncNeeded to check if a certain time has passed.
|
||||||
|
// However, since we barely send data for now just sync the course.
|
||||||
|
return this.syncCourse(courseId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize a course.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID to be synced.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||||
|
*/
|
||||||
|
async syncCourse(courseId: number, siteId?: string): Promise<CoreCourseSyncResult> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (this.isSyncing(courseId, siteId)) {
|
||||||
|
// There's already a sync ongoing for this discussion, return the promise.
|
||||||
|
return this.getOngoingSync(courseId, siteId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Try to sync course '${courseId}'`);
|
||||||
|
|
||||||
|
return this.addOngoingSync(courseId, this.syncCourseCompletion(courseId, siteId), siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync course offline completion.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID to be synced.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||||
|
*/
|
||||||
|
protected async syncCourseCompletion(courseId: number, siteId?: string): Promise<CoreCourseSyncResult> {
|
||||||
|
const result: CoreCourseSyncResult = {
|
||||||
|
warnings: [],
|
||||||
|
updated: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get offline responses to be sent.
|
||||||
|
const completions = await CoreUtils.instance.ignoreErrors(
|
||||||
|
CoreCourseOffline.instance.getCourseManualCompletions(courseId, siteId),
|
||||||
|
<CoreCourseManualCompletionDBRecord[]> [],
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (!completions || !completions.length) {
|
||||||
|
// Nothing to sync, set sync time.
|
||||||
|
await this.setSyncTime(String(courseId), siteId);
|
||||||
|
|
||||||
|
// All done, return the data.
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CoreApp.instance.isOnline()) {
|
||||||
|
// Cannot sync in offline.
|
||||||
|
throw new CoreNetworkError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current completion status to check if any completion was modified in web.
|
||||||
|
// This can be retrieved on core_course_get_contents since 3.6 but this is an easy way to get them.
|
||||||
|
const onlineCompletions = await CoreCourse.instance.getActivitiesCompletionStatus(
|
||||||
|
courseId,
|
||||||
|
siteId,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send all the completions.
|
||||||
|
await Promise.all(completions.map(async (entry) => {
|
||||||
|
const onlineComp = onlineCompletions[entry.cmid];
|
||||||
|
|
||||||
|
// Check if the completion was modified in online. If so, discard it.
|
||||||
|
if (onlineComp && onlineComp.timecompleted * 1000 > entry.timecompleted) {
|
||||||
|
await CoreCourseOffline.instance.deleteManualCompletion(entry.cmid, siteId);
|
||||||
|
|
||||||
|
// Completion deleted, add a warning if the completion status doesn't match.
|
||||||
|
if (onlineComp.state != entry.completed) {
|
||||||
|
result.warnings.push(Translate.instance.instant('core.course.warningofflinemanualcompletiondeleted', {
|
||||||
|
name: entry.coursename || courseId,
|
||||||
|
error: Translate.instance.instant('core.course.warningmanualcompletionmodified'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreCourse.instance.markCompletedManuallyOnline(entry.cmid, !!entry.completed, siteId);
|
||||||
|
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
await CoreCourseOffline.instance.deleteManualCompletion(entry.cmid, siteId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!CoreUtils.instance.isWebServiceError(error)) {
|
||||||
|
// Couldn't connect to server, reject.
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The WebService has thrown an error, this means that the completion cannot be submitted. Delete it.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
await CoreCourseOffline.instance.deleteManualCompletion(entry.cmid, siteId);
|
||||||
|
|
||||||
|
// Completion deleted, add a warning.
|
||||||
|
result.warnings.push(Translate.instance.instant('core.course.warningofflinemanualcompletiondeleted', {
|
||||||
|
name: entry.coursename || courseId,
|
||||||
|
error: CoreTextUtils.instance.getErrorMessageFromError(error),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (result.updated) {
|
||||||
|
try {
|
||||||
|
// Update data.
|
||||||
|
await CoreCourse.instance.invalidateSections(courseId, siteId);
|
||||||
|
|
||||||
|
const currentSite = CoreSites.instance.getCurrentSite();
|
||||||
|
|
||||||
|
if (currentSite?.isVersionGreaterEqualThan('3.6')) {
|
||||||
|
await CoreCourse.instance.getSections(courseId, false, true, undefined, siteId);
|
||||||
|
} else {
|
||||||
|
await CoreCourse.instance.getActivitiesCompletionStatus(courseId, siteId);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync finished, set sync time.
|
||||||
|
await this.setSyncTime(String(courseId), siteId);
|
||||||
|
|
||||||
|
// All done, return the data.
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreCourseSync extends makeSingleton(CoreCourseSyncProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of course sync.
|
||||||
|
*/
|
||||||
|
export type CoreCourseSyncResult = {
|
||||||
|
updated: boolean;
|
||||||
|
warnings: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data passed to AUTO_SYNCED event.
|
||||||
|
*/
|
||||||
|
export type CoreCourseAutoSyncData = CoreEventSiteData & {
|
||||||
|
courseId: number;
|
||||||
|
warnings: CoreWSExternalWarning[];
|
||||||
|
};
|
|
@ -26,6 +26,7 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider to provide some helper functions regarding files and packages.
|
* Provider to provide some helper functions regarding files and packages.
|
||||||
|
@ -152,7 +153,7 @@ export class CoreFileHelperProvider {
|
||||||
} else {
|
} else {
|
||||||
if (!isOnline && !this.isStateDownloaded(state)) {
|
if (!isOnline && !this.isStateDownloaded(state)) {
|
||||||
// Not downloaded and user is offline, reject.
|
// Not downloaded and user is offline, reject.
|
||||||
throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
|
|
|
@ -49,6 +49,7 @@ import {
|
||||||
SchemaVersionsDBEntry,
|
SchemaVersionsDBEntry,
|
||||||
} from '@services/database/sites';
|
} from '@services/database/sites';
|
||||||
import { CoreArray } from '../singletons/array';
|
import { CoreArray } from '../singletons/array';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
|
||||||
export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS');
|
export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS');
|
||||||
|
|
||||||
|
@ -136,7 +137,7 @@ export class CoreSitesProvider {
|
||||||
if (!CoreUrlUtils.instance.isHttpURL(siteUrl)) {
|
if (!CoreUrlUtils.instance.isHttpURL(siteUrl)) {
|
||||||
throw new CoreError(Translate.instance.instant('core.login.invalidsite'));
|
throw new CoreError(Translate.instance.instant('core.login.invalidsite'));
|
||||||
} else if (!CoreApp.instance.isOnline()) {
|
} else if (!CoreApp.instance.isOnline()) {
|
||||||
throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -359,7 +360,7 @@ export class CoreSitesProvider {
|
||||||
retry?: boolean,
|
retry?: boolean,
|
||||||
): Promise<CoreSiteUserTokenResponse> {
|
): Promise<CoreSiteUserTokenResponse> {
|
||||||
if (!CoreApp.instance.isOnline()) {
|
if (!CoreApp.instance.isOnline()) {
|
||||||
throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!service) {
|
if (!service) {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { CoreSilentError } from '@classes/errors/silenterror';
|
||||||
import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons';
|
import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
|
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* "Utils" service with helper functions for UI, DOM elements and HTML code.
|
* "Utils" service with helper functions for UI, DOM elements and HTML code.
|
||||||
|
@ -642,11 +643,13 @@ export class CoreDomUtilsProvider {
|
||||||
* Given a message, it deduce if it's a network error.
|
* Given a message, it deduce if it's a network error.
|
||||||
*
|
*
|
||||||
* @param message Message text.
|
* @param message Message text.
|
||||||
|
* @param error Error object.
|
||||||
* @return True if the message error is a network error, false otherwise.
|
* @return True if the message error is a network error, false otherwise.
|
||||||
*/
|
*/
|
||||||
protected isNetworkError(message: string): boolean {
|
protected isNetworkError(message: string, error?: CoreError | CoreTextErrorObject | string): boolean {
|
||||||
return message == Translate.instance.instant('core.networkerrormsg') ||
|
return message == Translate.instance.instant('core.networkerrormsg') ||
|
||||||
message == Translate.instance.instant('core.fileuploader.errormustbeonlinetoupload');
|
message == Translate.instance.instant('core.fileuploader.errormustbeonlinetoupload') ||
|
||||||
|
error instanceof CoreNetworkError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1365,7 +1368,7 @@ export class CoreDomUtilsProvider {
|
||||||
buttons: [Translate.instance.instant('core.ok')],
|
buttons: [Translate.instance.instant('core.ok')],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.isNetworkError(message)) {
|
if (this.isNetworkError(message, error)) {
|
||||||
alertOptions.cssClass = 'core-alert-network-error';
|
alertOptions.cssClass = 'core-alert-network-error';
|
||||||
} else {
|
} else {
|
||||||
alertOptions.header = Translate.instance.instant('core.error');
|
alertOptions.header = Translate.instance.instant('core.error');
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { CoreLogger } from '@singletons/logger';
|
||||||
import { CoreWSError } from '@classes/errors/wserror';
|
import { CoreWSError } from '@classes/errors/wserror';
|
||||||
import { CoreAjaxError } from '@classes/errors/ajaxerror';
|
import { CoreAjaxError } from '@classes/errors/ajaxerror';
|
||||||
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
|
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service allows performing WS calls and download/upload files.
|
* This service allows performing WS calls and download/upload files.
|
||||||
|
@ -107,7 +108,7 @@ export class CoreWSProvider {
|
||||||
if (!preSets) {
|
if (!preSets) {
|
||||||
throw new CoreError(Translate.instance.instant('core.unexpectederror'));
|
throw new CoreError(Translate.instance.instant('core.unexpectederror'));
|
||||||
} else if (!CoreApp.instance.isOnline()) {
|
} else if (!CoreApp.instance.isOnline()) {
|
||||||
throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
preSets.typeExpected = preSets.typeExpected || 'object';
|
preSets.typeExpected = preSets.typeExpected || 'object';
|
||||||
|
@ -249,7 +250,7 @@ export class CoreWSProvider {
|
||||||
this.logger.debug('Downloading file', url, path, addExtension);
|
this.logger.debug('Downloading file', url, path, addExtension);
|
||||||
|
|
||||||
if (!CoreApp.instance.isOnline()) {
|
if (!CoreApp.instance.isOnline()) {
|
||||||
throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a tmp path to download the file and then move it to final location.
|
// Use a tmp path to download the file and then move it to final location.
|
||||||
|
@ -741,7 +742,7 @@ export class CoreWSProvider {
|
||||||
if (!preSets) {
|
if (!preSets) {
|
||||||
throw new CoreError(Translate.instance.instant('core.unexpectederror'));
|
throw new CoreError(Translate.instance.instant('core.unexpectederror'));
|
||||||
} else if (!CoreApp.instance.isOnline()) {
|
} else if (!CoreApp.instance.isOnline()) {
|
||||||
throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
preSets.typeExpected = preSets.typeExpected || 'object';
|
preSets.typeExpected = preSets.typeExpected || 'object';
|
||||||
|
@ -825,7 +826,7 @@ export class CoreWSProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CoreApp.instance.isOnline()) {
|
if (!CoreApp.instance.isOnline()) {
|
||||||
throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadUrl = preSets.siteUrl + '/webservice/upload.php';
|
const uploadUrl = preSets.siteUrl + '/webservice/upload.php';
|
||||||
|
|
Loading…
Reference in New Issue