MOBILE-2309 contentlinks: Implement base handlers

main
Dani Palou 2018-01-23 09:08:49 +01:00
parent 1cfd38229f
commit b3d0457540
12 changed files with 411 additions and 36 deletions

View File

@ -0,0 +1,111 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { CoreContentLinksHandler, CoreContentLinksAction } from '../providers/delegate';
/**
* Base handler to be registered in CoreContentLinksHandler. It is useful to minimize the amount of
* functions that handlers need to implement.
*
* It allows you to specify a "pattern" (RegExp) that will be used to check if the handler handles a URL and to get its site URL.
*/
export class CoreContentLinksHandlerBase implements CoreContentLinksHandler {
/**
* A name to identify the handler.
* @type {string}
*/
name = 'CoreContentLinksHandlerBase';
/**
* Handler's priority. The highest priority is treated first.
* @type {number}
*/
priority = 0;
/**
* Whether the isEnabled function should be called for all the users in a site. It should be true only if the isEnabled call
* can return different values for different users in same site.
* @type {boolean}
*/
checkAllUsers = false;
/**
* Name of the feature this handler is related to.
* It will be used to check if the feature is disabled (@see CoreSite.isFeatureDisabled).
* @type {string}
*/
featureName = '';
/**
* A pattern to use to detect if the handler handles a URL and to get its site URL. Required if "handles" and
* "getSiteUrl" functions aren't overridden.
* @type {RexExp}
*/
pattern?: RegExp;
constructor() {}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
return [];
}
/**
* Check if a URL is handled by this handler.
*
* @param {string} url The URL to check.
* @return {boolean} Whether the URL is handled by this handler
*/
handles(url: string) : boolean {
return this.pattern && url.search(this.pattern) >= 0;
}
/**
* If the URL is handled by this handler, return the site URL.
*
* @param {string} url The URL to check.
* @return {string} Site URL if it is handled, undefined otherwise.
*/
getSiteUrl(url: string) : string {
if (this.pattern) {
var position = url.search(this.pattern);
if (position > -1) {
return url.substr(0, position);
}
}
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise<boolean> {
return true;
}
}

View File

@ -0,0 +1,101 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { CoreContentLinksAction } from '../providers/delegate';
import { CoreContentLinksHandlerBase } from './base-handler';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreCourseHelperProvider } from '../../course/providers/helper';
/**
* Handler to handle URLs pointing to the grade of a module.
*/
export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerBase {
/**
* Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled.
* @type {string}
*/
addon: string;
/**
* Name of the module (assign, book, ...).
* @type {string}
*/
modName: string;
/**
* Whether the module can be reviewed in the app. If true, the handler needs to implement the goToReview function.
* @type {boolean}
*/
canReview: boolean;
constructor(protected courseHelper: CoreCourseHelperProvider, protected domUtils: CoreDomUtilsProvider,
protected sitesProvider: CoreSitesProvider) {
super();
// Match the grade.php URL with an id param.
this.pattern = new RegExp('\/mod\/' + this.modName + '\/grade\.php.*([\&\?]id=\\d+)');
this.featureName = '$mmCourseDelegate_' + this.addon;
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
courseId = courseId || params.courseid || params.cid;
return [{
action: (siteId) : void => {
// Check if userid is the site's current user.
const modal = this.domUtils.showModalLoading();
this.sitesProvider.getSite(siteId).then((site) => {
if (!params.userid || params.userid == site.getUserId()) {
// No user specified or current user. Navigate to module.
this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId);
} else if (this.canReview) {
// Use the goToReview function.
this.goToReview(url, params, courseId, siteId);
} else {
// Not current user and cannot review it in the app, open it in browser.
site.openInBrowserWithAutoLogin(url);
}
}).finally(() => {
modal.dismiss();
});
}
}];
}
/**
* Go to the page to review.
*
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} courseId Course ID related to the URL.
* @param {string} siteId List of sites the URL belongs to.
* @return {Promise<any>} Promise resolved when done.
*/
protected goToReview(url: string, params: any, courseId: number, siteId: string) : Promise<any> {
// This function should be overridden.
return Promise.resolve();
}
}

View File

@ -0,0 +1,63 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { CoreContentLinksAction } from '../providers/delegate';
import { CoreContentLinksHandlerBase } from './base-handler';
import { CoreCourseHelperProvider } from '../../course/providers/helper';
/**
* Handler to handle URLs pointing to the index of a module.
*/
export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerBase {
/**
* Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled.
* @type {string}
*/
addon: string;
/**
* Name of the module (assign, book, ...).
* @type {string}
*/
modName: string;
constructor(private courseHelper: CoreCourseHelperProvider) {
super();
// Match the view.php URL with an id param.
this.pattern = new RegExp('\/mod\/' + this.modName + '\/view\.php.*([\&\?]id=\\d+)');
this.featureName = '$mmCourseDelegate_' + this.addon;
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
courseId = courseId || params.courseid || params.cid;
return [{
action: (siteId) => {
this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId);
}
}];
}
}

View File

@ -68,7 +68,6 @@ export interface CoreContentLinksHandler {
*/
handles(url: string) : boolean;
/**
* If the URL is handled by this handler, return the site URL.
*

View File

@ -42,6 +42,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
@Input() course: any; // The course to render.
@Input() sections: any[]; // List of course sections.
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
@Input() initialSectionId: number; // The section to load first.
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when any module completion changes.
// Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
@ -142,11 +143,24 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: {[name: string]: SimpleChange}) {
if (changes.sections && this.sections) {
if (!this.selectedSection) {
// There is no selected section yet, calculate which one to get.
this.cfDelegate.getCurrentSection(this.course, this.sections).then((section) => {
this.loaded = true;
this.sectionChanged(section);
});
// There is no selected section yet, calculate which one to load.
if (this.initialSectionId) {
// We have an input indicating the section ID to load. Search the section.
for (let i = 0; i < this.sections.length; i++) {
let section = this.sections[i];
if (section.id == this.initialSectionId) {
this.loaded = true;
this.sectionChanged(section);
break;
}
}
} else {
// No section specified, get current section.
this.cfDelegate.getCurrentSection(this.course, this.sections).then((section) => {
this.loaded = true;
this.sectionChanged(section);
});
}
} else {
// We have a selected section, but the list has changed. Search the section in the list.
let newSection;
@ -214,6 +228,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// Set the Input data.
this.componentInstances[type].course = this.course;
this.componentInstances[type].sections = this.sections;
this.componentInstances[type].initialSectionId = this.initialSectionId;
this.componentInstances[type].downloadEnabled = this.downloadEnabled;
this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed.

View File

@ -21,6 +21,6 @@
<a aria-selected="true">{{ 'core.course.contents' | translate }}</a>
<a *ngFor="let handler of courseHandlers">{{ handler.data.title || translate }}</a>
</div>
<core-course-format [course]="course" [sections]="sections" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format>
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format>
</core-loading>
</ion-content>

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Component, ViewChild, OnDestroy } from '@angular/core';
import { IonicPage, NavParams, Content } from 'ionic-angular';
import { IonicPage, NavParams, Content, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '../../../../providers/events';
import { CoreSitesProvider } from '../../../../providers/sites';
@ -39,6 +39,7 @@ export class CoreCourseSectionPage implements OnDestroy {
title: string;
course: any;
sections: any[];
sectionId: number;
courseHandlers: CoreCoursesHandlerToDisplay[];
dataLoaded: boolean;
downloadEnabled: boolean;
@ -47,18 +48,17 @@ export class CoreCourseSectionPage implements OnDestroy {
prefetchCourseIcon: 'spinner'
};
protected moduleId;
protected completionObserver;
protected courseStatusObserver;
protected isDestroyed = false;
constructor(navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
constructor(private navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
private courseFormatDelegate: CoreCourseFormatDelegate, private coursesDelegate: CoreCoursesDelegate,
private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider,
private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider,
sitesProvider: CoreSitesProvider) {
sitesProvider: CoreSitesProvider, private navCtrl: NavController) {
this.course = navParams.get('course');
this.moduleId = navParams.get('moduleId');
this.sectionId = navParams.get('sectionId');
// Get the title to display. We dont't have sections yet.
this.title = courseFormatDelegate.getCourseTitle(this.course);
@ -81,9 +81,14 @@ export class CoreCourseSectionPage implements OnDestroy {
* View loaded.
*/
ionViewDidLoad() {
let module = this.navParams.get('module');
if (module) {
this.courseHelper.openModule(this.navCtrl, module, this.course.id, this.sectionId);
}
this.loadData().finally(() => {
this.dataLoaded = true;
delete this.moduleId; // Only load module automatically the first time.
// Determine the course prefetch status.
this.determineCoursePrefetchIcon().then(() => {
@ -133,7 +138,7 @@ export class CoreCourseSectionPage implements OnDestroy {
promises.push(promise.then((completionStatus) => {
// Get all the sections.
promises.push(this.courseProvider.getSections(this.course.id, false, true).then((sections) => {
this.courseHelper.addHandlerDataForModules(sections, this.course.id, this.moduleId, completionStatus);
this.courseHelper.addHandlerDataForModules(sections, this.course.id, completionStatus);
// Format the name of each section and check if it has content.
this.sections = sections.map((section) => {

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreFilepoolProvider } from '../../../providers/filepool';
import { CoreSitesProvider } from '../../../providers/sites';
@ -21,10 +22,13 @@ import { CoreTextUtilsProvider } from '../../../providers/utils/text';
import { CoreTimeUtilsProvider } from '../../../providers/utils/time';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreCoursesDelegate, CoreCoursesHandlerToDisplay } from '../../courses/providers/delegate';
import { CoreSiteHomeProvider } from '../../sitehome/providers/sitehome';
import { CoreCourseProvider } from './course';
import { CoreCourseModuleDelegate } from './module-delegate';
import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from './module-prefetch-delegate';
import { CoreLoginHelperProvider } from '../../login/providers/helper';
import { CoreConstants } from '../../constants';
import { CoreSite } from '../../../classes/site';
import * as moment from 'moment';
/**
@ -109,7 +113,8 @@ export class CoreCourseHelperProvider {
private moduleDelegate: CoreCourseModuleDelegate, private prefetchDelegate: CoreCourseModulePrefetchDelegate,
private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider,
private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
private utils: CoreUtilsProvider, private translate: TranslateService, private coursesDelegate: CoreCoursesDelegate) {}
private utils: CoreUtilsProvider, private translate: TranslateService, private coursesDelegate: CoreCoursesDelegate,
private loginHelper: CoreLoginHelperProvider, private siteHomeProvider: CoreSiteHomeProvider) {}
/**
* This function treats every module on the sections provided to load the handler data, treat completion
@ -117,11 +122,10 @@ export class CoreCourseHelperProvider {
*
* @param {any[]} sections List of sections to treat modules.
* @param {number} courseId Course ID of the modules.
* @param {number} [moduleId] Module to navigate to if needed.
* @param {any[]} [completionStatus] List of completion status.
* @return {boolean} Whether the sections have content.
*/
addHandlerDataForModules(sections: any[], courseId: number, moduleId?: number, completionStatus?: any) {
addHandlerDataForModules(sections: any[], courseId: number, completionStatus?: any) {
let hasContent = false;
sections.forEach((section) => {
@ -139,11 +143,6 @@ export class CoreCourseHelperProvider {
module.completionstatus = completionStatus[module.id];
module.completionstatus.courseId = courseId;
}
if (module.id == moduleId) {
// This is the module we're looking for. Open it.
module.handlerData.action(new Event('click'), module, courseId);
}
});
});
@ -578,6 +577,86 @@ export class CoreCourseHelperProvider {
return 'Section-' + section.id;
}
/**
* Navigate to a module.
*
* @param {number} moduleId Module's ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [courseId] Course ID. If not defined we'll try to retrieve it from the site.
* @param {number} [sectionId] Section the module belongs to. If not defined we'll try to retrieve it from the site.
* @return {Promise<void>} Promise resolved when done.
*/
navigateToModule(moduleId: number, siteId?: string, courseId?: number, sectionId?: number) : Promise<void> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
let modal = this.domUtils.showModalLoading(),
promise,
site: CoreSite;
if (courseId && sectionId) {
// No need to retrieve more data.
promise = Promise.resolve();
} else if (!courseId) {
// We don't have courseId.
promise = this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => {
courseId = module.course;
sectionId = module.section;
});
} else {
// We don't have sectionId but we have courseId.
promise = this.courseProvider.getModuleSectionId(moduleId, siteId).then((id) => {
sectionId = id;
});
}
return promise.then(() => {
// Get the site.
return this.sitesProvider.getSite(siteId);
}).then((s) => {
site = s;
// Get the module.
return this.courseProvider.getModule(moduleId, courseId, sectionId, false, false, siteId);
}).then((module) => {
const params = {
course: {id: courseId},
module: module,
sectionId: sectionId
};
module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId);
if (courseId == site.getSiteHomeId()) {
// Check if site home is available.
return this.siteHomeProvider.isAvailable().then(() => {
this.loginHelper.redirect('CoreSiteHomeIndexPage', params, siteId);
});
} else {
this.loginHelper.redirect('CoreCourseSectionPage', params, siteId);
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
}).finally(() => {
modal.dismiss();
});
}
/**
* Open a module.
*
* @param {NavController} navCtrl The NavController to use.
* @param {any} module The module to open.
* @param {number} courseId The course ID of the module.
* @param {number} [sectionId] The section ID of the module.
*/
openModule(navCtrl: NavController, module: any, courseId: number, sectionId?: number) : void {
if (!module.handlerData) {
module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId);
}
module.handlerData.action(new Event('click'), navCtrl, module, courseId, {animate: false});
}
/**
* Prefetch all the activities in a course and also the course addons.
*

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { NavController } from 'ionic-angular';
import { NavController, NavOptions } from 'ionic-angular';
import { CoreEventsProvider } from '../../../providers/events';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
@ -104,8 +104,9 @@ export interface CoreCourseModuleHandlerData {
* @param {NavController} navCtrl NavController instance.
* @param {any} module The module object.
* @param {number} courseId The course ID.
* @param {NavOptions} [options] Options for the navigation.
*/
action?(event: Event, navCtrl: NavController, module: any, courseId: number) : void;
action?(event: Event, navCtrl: NavController, module: any, courseId: number, options?: NavOptions) : void;
};
/**
@ -208,11 +209,11 @@ export class CoreCourseModuleDelegate {
icon: this.courseProvider.getModuleIconSrc(module.modname),
title: module.name,
class: 'core-course-default-handler core-course-module-' + module.modname + '-handler',
action: (event: Event, navCtrl: NavController, module: any, courseId: number) => {
action: (event: Event, navCtrl: NavController, module: any, courseId: number, options?: NavOptions) => {
event.preventDefault();
event.stopPropagation();
navCtrl.push('CoreCourseUnsupportedModulePage', {module: module});
navCtrl.push('CoreCourseUnsupportedModulePage', {module: module}, options);
}
};

View File

@ -28,8 +28,6 @@ import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/modu
templateUrl: 'index.html',
})
export class CoreSiteHomeIndexComponent implements OnInit {
@Input() moduleId?: number;
dataLoaded: boolean;
section: any;
block: any;
@ -134,8 +132,7 @@ export class CoreSiteHomeIndexComponent implements OnInit {
this.block.hasContent = this.courseHelper.sectionHasContent(this.block);
}
this.hasContent = this.courseHelper.addHandlerDataForModules(this.sectionsLoaded, this.siteHomeId, this.moduleId) ||
this.hasContent;
this.hasContent = this.courseHelper.addHandlerDataForModules(this.sectionsLoaded, this.siteHomeId) || this.hasContent;
// Add log in Moodle.
this.courseProvider.logView(this.siteHomeId);

View File

@ -3,4 +3,4 @@
<ion-title>{{ 'core.sitehome.sitehome' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<core-sitehome-index [moduleId]="moduleId"></core-sitehome-index>
<core-sitehome-index></core-sitehome-index>

View File

@ -13,7 +13,9 @@
// limitations under the License.
import { Component } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { IonicPage, NavParams, NavController } from 'ionic-angular';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
/**
* Page that displays site home index.
@ -25,9 +27,11 @@ import { IonicPage, NavParams } from 'ionic-angular';
})
export class CoreSiteHomeIndexPage {
moduleId: number;
constructor(navParams: NavParams) {
this.moduleId = navParams.get('moduleId');
constructor(navParams: NavParams, navCtrl: NavController, courseHelper: CoreCourseHelperProvider,
sitesProvider: CoreSitesProvider) {
let module = navParams.get('module');
if (module) {
courseHelper.openModule(navCtrl, module, sitesProvider.getCurrentSite().getSiteHomeId());
}
}
}