From 3cb62c748cb17946120f4e48d66b7873a5bf8f84 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Jan 2018 11:26:21 +0100 Subject: [PATCH] MOBILE-2309 contentlinks: Implement some handlers and handle links --- src/app/app.module.ts | 2 + .../classes/module-grade-handler.ts | 10 +- .../classes/module-index-handler.ts | 2 +- .../pages/choose-site/choose-site.ts | 2 +- src/core/contentlinks/providers/delegate.ts | 4 +- src/core/contentlinks/providers/helper.ts | 9 +- src/core/course/components/format/format.ts | 8 +- src/core/course/pages/section/section.html | 2 +- src/core/course/pages/section/section.ts | 2 + .../overview-events/overview-events.ts | 24 +- src/core/courses/courses.module.ts | 17 +- .../courses/providers/course-link-handler.ts | 267 ++++++++++++++++++ .../providers/courses-index-link-handler.ts | 64 +++++ .../providers/my-overview-link-handler.ts | 52 ++++ .../login/pages/credentials/credentials.ts | 24 +- .../sitehome/providers/index-link-handler.ts | 84 ++++++ src/core/sitehome/sitehome.module.ts | 9 +- src/directives/format-text.ts | 9 +- src/directives/link.ts | 14 +- 19 files changed, 551 insertions(+), 54 deletions(-) create mode 100644 src/core/courses/providers/course-link-handler.ts create mode 100644 src/core/courses/providers/courses-index-link-handler.ts create mode 100644 src/core/courses/providers/my-overview-link-handler.ts create mode 100644 src/core/sitehome/providers/index-link-handler.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0ea058335..83e483cc6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -58,6 +58,7 @@ import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module'; import { CoreCourseModule } from '../core/course/course.module'; import { CoreSiteHomeModule } from '../core/sitehome/sitehome.module'; +import { CoreContentLinksModule } from '../core/contentlinks/contentlinks.module'; // Addon modules. import { AddonCalendarModule } from '../addon/calendar/calendar.module'; @@ -94,6 +95,7 @@ export function createTranslateLoader(http: HttpClient) { CoreSharedFilesModule, CoreCourseModule, CoreSiteHomeModule, + CoreContentLinksModule, AddonCalendarModule ], bootstrap: [IonicApp], diff --git a/src/core/contentlinks/classes/module-grade-handler.ts b/src/core/contentlinks/classes/module-grade-handler.ts index 15866a9a5..a6f8bf3c0 100644 --- a/src/core/contentlinks/classes/module-grade-handler.ts +++ b/src/core/contentlinks/classes/module-grade-handler.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { NavController } from 'ionic-angular'; import { CoreContentLinksAction } from '../providers/delegate'; import { CoreContentLinksHandlerBase } from './base-handler'; import { CoreSitesProvider } from '../../../providers/sites'; @@ -64,7 +65,7 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB courseId = courseId || params.courseid || params.cid; return [{ - action: (siteId) : void => { + action: (siteId, navCtrl?) : void => { // Check if userid is the site's current user. const modal = this.domUtils.showModalLoading(); this.sitesProvider.getSite(siteId).then((site) => { @@ -73,7 +74,7 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId); } else if (this.canReview) { // Use the goToReview function. - this.goToReview(url, params, courseId, siteId); + this.goToReview(url, params, courseId, siteId, navCtrl); } else { // Not current user and cannot review it in the app, open it in browser. site.openInBrowserWithAutoLogin(url); @@ -91,10 +92,11 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB * @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. + * @param {string} siteId Site to use. + * @param {NavController} [navCtrl] Nav Controller to use to navigate. * @return {Promise} Promise resolved when done. */ - protected goToReview(url: string, params: any, courseId: number, siteId: string) : Promise { + protected goToReview(url: string, params: any, courseId: number, siteId: string, navCtrl?: NavController) : Promise { // This function should be overridden. return Promise.resolve(); } diff --git a/src/core/contentlinks/classes/module-index-handler.ts b/src/core/contentlinks/classes/module-index-handler.ts index 1d7a5819f..89c1c4aef 100644 --- a/src/core/contentlinks/classes/module-index-handler.ts +++ b/src/core/contentlinks/classes/module-index-handler.ts @@ -55,7 +55,7 @@ export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerB courseId = courseId || params.courseid || params.cid; return [{ - action: (siteId) => { + action: (siteId, navCtrl?) => { this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId); } }]; diff --git a/src/core/contentlinks/pages/choose-site/choose-site.ts b/src/core/contentlinks/pages/choose-site/choose-site.ts index 2f5ed59c6..7c71471b6 100644 --- a/src/core/contentlinks/pages/choose-site/choose-site.ts +++ b/src/core/contentlinks/pages/choose-site/choose-site.ts @@ -80,7 +80,7 @@ export class CoreContentLinksChooseSitePage implements OnInit { * @param {string} siteId Site ID. */ siteClicked(siteId: string) : void { - this.action.action(siteId); + this.action.action(siteId, this.navCtrl); } /** diff --git a/src/core/contentlinks/providers/delegate.ts b/src/core/contentlinks/providers/delegate.ts index b9fbced5d..6ab53e226 100644 --- a/src/core/contentlinks/providers/delegate.ts +++ b/src/core/contentlinks/providers/delegate.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreLoggerProvider } from '../../../providers/logger'; import { CoreSitesProvider } from '../../../providers/sites'; import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; @@ -115,8 +116,9 @@ export interface CoreContentLinksAction { * Action to perform when the link is clicked. * * @param {string} siteId The site ID. + * @param {NavController} [navCtrl] Nav Controller to use to navigate. */ - action(siteId: string) : void; + action(siteId: string, navCtrl?: NavController) : void; }; /** diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts index 2d0fb6853..3033cc368 100644 --- a/src/core/contentlinks/providers/helper.ts +++ b/src/core/contentlinks/providers/helper.ts @@ -202,9 +202,10 @@ export class CoreContentLinksHelperProvider { * @param {string} url URL to handle. * @param {string} [username] Username related with the URL. E.g. in 'http://myuser@m.com', url would be 'http://m.com' and * the username 'myuser'. Don't use it if you don't want to filter by username. + * @param {NavController} [navCtrl] Nav Controller to use to navigate. * @return {Promise} Promise resolved with a boolean: true if URL was treated, false otherwise. */ - handleLink(url: string, username?: string) : Promise { + handleLink(url: string, username?: string, navCtrl?: NavController) : Promise { // Check if the link should be treated by some component/addon. return this.contentLinksDelegate.getActionsFor(url, undefined, username).then((actions) => { const action = this.getFirstValidAction(actions); @@ -212,18 +213,18 @@ export class CoreContentLinksHelperProvider { if (!this.sitesProvider.isLoggedIn()) { // No current site. Perform the action if only 1 site found, choose the site otherwise. if (action.sites.length == 1) { - action.action(action.sites[0]); + action.action(action.sites[0], navCtrl); } else { this.goToChooseSite(url); } } else if (action.sites.length == 1 && action.sites[0] == this.sitesProvider.getCurrentSiteId()) { // Current site. - action.action(action.sites[0]); + action.action(action.sites[0], navCtrl); } else { // Not current site or more than one site. Ask for confirmation. this.domUtils.showConfirm(this.translate.instant('core.contentlinks.confirmurlothersite')).then(() => { if (action.sites.length == 1) { - action.action(action.sites[0]); + action.action(action.sites[0], navCtrl); } else { this.goToChooseSite(url); } diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index fd7fd2d30..20d84c694 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -42,7 +42,8 @@ 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. + @Input() initialSectionId?: number; // The section to load first (by ID). + @Input() initialSectionNumber?: number; // The section to load first (by number). @Output() completionChanged?: EventEmitter; // 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. @@ -144,11 +145,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { if (changes.sections && this.sections) { if (!this.selectedSection) { // There is no selected section yet, calculate which one to load. - if (this.initialSectionId) { + if (this.initialSectionId || this.initialSectionNumber) { // 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) { + if (section.id == this.initialSectionId || section.section == this.initialSectionNumber) { this.loaded = true; this.sectionChanged(section); break; @@ -229,6 +230,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.componentInstances[type].course = this.course; this.componentInstances[type].sections = this.sections; this.componentInstances[type].initialSectionId = this.initialSectionId; + this.componentInstances[type].initialSectionNumber = this.initialSectionNumber; this.componentInstances[type].downloadEnabled = this.downloadEnabled; this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed. diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index f81e941c9..f890b24f3 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -21,6 +21,6 @@ {{ 'core.course.contents' | translate }} {{ handler.data.title || translate }} - + diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 94b107cfa..b8fb69a58 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -40,6 +40,7 @@ export class CoreCourseSectionPage implements OnDestroy { course: any; sections: any[]; sectionId: number; + sectionNumber: number; courseHandlers: CoreCoursesHandlerToDisplay[]; dataLoaded: boolean; downloadEnabled: boolean; @@ -59,6 +60,7 @@ export class CoreCourseSectionPage implements OnDestroy { sitesProvider: CoreSitesProvider, private navCtrl: NavController) { this.course = navParams.get('course'); this.sectionId = navParams.get('sectionId'); + this.sectionNumber = navParams.get('sectionNumber'); // Get the title to display. We dont't have sections yet. this.title = courseFormatDelegate.getCourseTitle(this.course); diff --git a/src/core/courses/components/overview-events/overview-events.ts b/src/core/courses/components/overview-events/overview-events.ts index df8db57cb..6e67a0149 100644 --- a/src/core/courses/components/overview-events/overview-events.ts +++ b/src/core/courses/components/overview-events/overview-events.ts @@ -13,11 +13,13 @@ // limitations under the License. import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreSitesProvider } from '../../../../providers/sites'; import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; import { CoreUtilsProvider } from '../../../../providers/utils/utils'; import { CoreCourseProvider } from '../../../course/providers/course'; +import { CoreContentLinksHelperProvider } from '../../../contentlinks/providers/helper'; import * as moment from 'moment'; /** @@ -41,9 +43,9 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges { next30Days: any[] = []; future: any[] = []; - constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, + constructor(private navCtrl: NavController, private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider, - private courseProvider: CoreCourseProvider) { + private courseProvider: CoreCourseProvider, private contentLinksHelper: CoreContentLinksHelperProvider) { this.loadMore = new EventEmitter(); } @@ -100,9 +102,6 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges { loadMoreEvents() { this.loadingMore = true; this.loadMore.emit(); - // this.loadMore().finally(function() { - // scope.loadingMore = false; - // }); } /** @@ -119,19 +118,14 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges { url = this.textUtils.decodeHTMLEntities(url); let modal = this.domUtils.showModalLoading(); - this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url).finally(() => { + this.contentLinksHelper.handleLink(url, undefined, this.navCtrl).then((treated) => { + if (!treated) { + return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); + } + }).finally(() => { modal.dismiss(); }); - // @todo - // $mmContentLinksHelper.handleLink(url).then((treated) => { - // if (!treated) { - // return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); - // } - // }).finally(() => { - // modal.dismiss(); - // }); - return false; } } diff --git a/src/core/courses/courses.module.ts b/src/core/courses/courses.module.ts index 8dfd4bd1f..cd603bae5 100644 --- a/src/core/courses/courses.module.ts +++ b/src/core/courses/courses.module.ts @@ -17,7 +17,11 @@ import { CoreCoursesProvider } from './providers/courses'; import { CoreCoursesMainMenuHandler } from './providers/mainmenu-handler'; import { CoreCoursesMyOverviewProvider } from './providers/my-overview'; import { CoreCoursesDelegate } from './providers/delegate'; +import { CoreCoursesCourseLinkHandler } from './providers/course-link-handler'; +import { CoreCoursesIndexLinkHandler } from './providers/courses-index-link-handler'; +import { CoreCoursesMyOverviewLinkHandler } from './providers/my-overview-link-handler'; import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; +import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; @NgModule({ declarations: [], @@ -27,12 +31,21 @@ import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; CoreCoursesProvider, CoreCoursesMainMenuHandler, CoreCoursesMyOverviewProvider, - CoreCoursesDelegate + CoreCoursesDelegate, + CoreCoursesCourseLinkHandler, + CoreCoursesIndexLinkHandler, + CoreCoursesMyOverviewLinkHandler ], exports: [] }) export class CoreCoursesModule { - constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreCoursesMainMenuHandler) { + constructor(mainMenuDelegate: CoreMainMenuDelegate, contentLinksDelegate: CoreContentLinksDelegate, + mainMenuHandler: CoreCoursesMainMenuHandler, courseLinkHandler: CoreCoursesCourseLinkHandler, + indexLinkHandler: CoreCoursesIndexLinkHandler, myOverviewLinkHandler: CoreCoursesMyOverviewLinkHandler) { mainMenuDelegate.registerHandler(mainMenuHandler); + + contentLinksDelegate.registerHandler(courseLinkHandler); + contentLinksDelegate.registerHandler(indexLinkHandler); + contentLinksDelegate.registerHandler(myOverviewLinkHandler); } } diff --git a/src/core/courses/providers/course-link-handler.ts b/src/core/courses/providers/course-link-handler.ts new file mode 100644 index 000000000..fa1fa3235 --- /dev/null +++ b/src/core/courses/providers/course-link-handler.ts @@ -0,0 +1,267 @@ +// (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 { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreCourseProvider } from '../../course/providers/course'; +import { CoreCoursesProvider } from './courses'; + +/** + * Handler to treat links to course view or enrol (except site home). + */ +@Injectable() +export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreCoursesCourseLinkHandler'; + pattern = /((\/enrol\/index\.php)|(\/course\/enrol\.php)|(\/course\/view\.php)).*([\?\&]id=\d+)/; + + protected waitStart = 0; + + constructor(private sitesProvider: CoreSitesProvider, private coursesProvider: CoreCoursesProvider, + private loginHelper: CoreLoginHelperProvider, private domUtils: CoreDomUtilsProvider, + private translate: TranslateService, private courseProvider: CoreCourseProvider) { + super(); + } + + /** + * 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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + courseId = parseInt(params.id, 10); + + let sectionId = params.sectionid ? parseInt(params.sectionid, 10) : null, + sectionNumber = typeof params.section != 'undefined' ? parseInt(params.section, 10) : NaN, + pageParams: any = { + course: {id: courseId}, + sectionId: sectionId || null + }; + + if (!isNaN(sectionNumber)) { + pageParams.sectionNumber = sectionNumber; + } + + return [{ + action: (siteId, navCtrl?) => { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + if (siteId == this.sitesProvider.getCurrentSiteId()) { + this.actionEnrol(courseId, url, pageParams).catch(() => { + // Ignore errors. + }); + } else { + // Use redirect to make the course the new history root (to avoid "loops" in history). + this.loginHelper.redirect('CoreCourseSectionPage', pageParams, siteId); + } + } + }]; + } + + /** + * 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} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise { + courseId = parseInt(params.id, 10); + + if (!courseId) { + return false; + } + + // Get the course id of Site Home. + return this.sitesProvider.getSiteHomeId(siteId).then((siteHomeId) => { + return courseId != siteHomeId; + }); + } + + /** + * Action to perform when an enrol link is clicked. + * + * @param {number} courseId Course ID. + * @param {string} url Treated URL. + * @param {any} pageParams Params to send to the new page. + * @return {Promise} Promise resolved when done. + */ + protected actionEnrol(courseId: number, url: string, pageParams: any) : Promise { + let modal = this.domUtils.showModalLoading(), + isEnrolUrl = !!url.match(/(\/enrol\/index\.php)|(\/course\/enrol\.php)/); + + // Check if user is enrolled in the course. + return this.coursesProvider.getUserCourse(courseId).catch(() => { + // User is not enrolled in the course. Check if can self enrol. + return this.canSelfEnrol(courseId).then(() => { + modal.dismiss(); + + // The user can self enrol. If it's not a enrolment URL we'll ask for confirmation. + let promise = isEnrolUrl ? Promise.resolve() : + this.domUtils.showConfirm(this.translate.instant('core.courses.confirmselfenrol')); + + return promise.then(() => { + // Enrol URL or user confirmed. + return this.selfEnrol(courseId).catch((error) => { + if (error) { + this.domUtils.showErrorModal(error); + } + return Promise.reject(null); + }); + }, () => { + // User cancelled. Check if the user can view the course contents (guest access or similar). + return this.courseProvider.getSections(courseId, false, true); + }); + }, (error) => { + // Can't self enrol. Check if the user can view the course contents (guest access or similar). + return this.courseProvider.getSections(courseId, false, true).catch(() => { + // Error. Show error message and allow the user to open the link in browser. + modal.dismiss(); + + if (error) { + error = error.message || error.error || error.content || error.body || error; + } + if (!error) { + error = this.translate.instant('core.courses.notenroled'); + } + + let body = this.translate.instant('core.twoparagraphs', + {p1: error, p2: this.translate.instant('core.confirmopeninbrowser')}); + this.domUtils.showConfirm(body).then(() => { + this.sitesProvider.getCurrentSite().openInBrowserWithAutoLogin(url); + }).catch(() => { + // User cancelled. + }); + return Promise.reject(null); + }); + }); + }).then(() => { + modal.dismiss(); + + // Use redirect to make the course the new history root (to avoid "loops" in history). + this.loginHelper.redirect('CoreCourseSectionPage', pageParams, this.sitesProvider.getCurrentSiteId()); + }); + } + + /** + * Check if a user can be "automatically" self enrolled in a course. + * + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved if user can be enrolled in a course, rejected otherwise. + */ + protected canSelfEnrol(courseId: number) : Promise { + // Check that the course has self enrolment enabled. + return this.coursesProvider.getCourseEnrolmentMethods(courseId).then((methods) => { + let isSelfEnrolEnabled = false, + instances = 0; + + methods.forEach((method) => { + if (method.type == 'self' && method.status) { + isSelfEnrolEnabled = true; + instances++; + } + }); + + if (!isSelfEnrolEnabled || instances != 1) { + // Self enrol not enabled or more than one instance. + return Promise.reject(null); + } + }); + } + + /** + * Try to self enrol a user in a course. + * + * @param {number} courseId Course ID. + * @param {string} [password] Password. + * @return {Promise} Promise resolved when the user is enrolled, rejected otherwise. + */ + protected selfEnrol(courseId: number, password?: string) : Promise { + const modal = this.domUtils.showModalLoading(); + return this.coursesProvider.selfEnrol(courseId, password).then(() => { + // Success self enrolling the user, invalidate the courses list. + return this.coursesProvider.invalidateUserCourses().catch(() => { + // Ignore errors. + }).then(() => { + // Sometimes the list of enrolled courses takes a while to be updated. Wait for it. + return this.waitForEnrolled(courseId, true).finally(() => { + modal.dismiss(); + }); + }); + + }).catch((error) => { + modal.dismiss(); + if (error && error.code === CoreCoursesProvider.ENROL_INVALID_KEY) { + // Invalid password. Allow the user to input password. + let title = this.translate.instant('core.courses.selfenrolment'), + body = ' ', // Empty message. + placeholder = this.translate.instant('core.courses.password'); + + if (typeof password != 'undefined') { + // The user attempted a password. Show an error message. + this.domUtils.showErrorModal(error.message); + } + + return this.domUtils.showPrompt(body, title, placeholder).then((password) => { + return this.selfEnrol(courseId, password); + }); + } else { + return Promise.reject(error); + } + }); + } + + /** + * Wait for the user to be enrolled in a course. + * + * @param {number} courseId The course ID. + * @param {boolean} first If it's the first call (true) or it's a recursive call (false). + * @return {Promise} Promise resolved when enrolled or timeout. + */ + protected waitForEnrolled(courseId: number, first?: boolean) : Promise { + if (first) { + this.waitStart = Date.now(); + } + + // Check if user is enrolled in the course. + return this.coursesProvider.invalidateUserCourses().catch(() => { + // Ignore errors. + }).then(() => { + return this.coursesProvider.getUserCourse(courseId); + }).catch(() => { + // Not enrolled, wait a bit and try again. + if (Date.now() - this.waitStart > 60000) { + // Max time reached, stop. + return; + } + + return new Promise((resolve, reject) => { + setTimeout(() => { + this.waitForEnrolled(courseId).then(resolve); + }, 5000); + }); + }); + } +} diff --git a/src/core/courses/providers/courses-index-link-handler.ts b/src/core/courses/providers/courses-index-link-handler.ts new file mode 100644 index 000000000..051adf797 --- /dev/null +++ b/src/core/courses/providers/courses-index-link-handler.ts @@ -0,0 +1,64 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreCoursesProvider } from './courses'; + +/** + * Handler to treat links to course index (list of courses). + */ +@Injectable() +export class CoreCoursesIndexLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreCoursesIndexLinkHandler'; + featureName = '$mmSideMenuDelegate_mmCourses'; + pattern = /\/course\/?(index\.php.*)?$/; + + constructor(private coursesProvider: CoreCoursesProvider, private loginHelper: CoreLoginHelperProvider) { + super(); + } + + /** + * 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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + return [{ + action: (siteId, navCtrl?) => { + var page = 'CoreCoursesMyCoursesPage', // By default, go to My Courses. + pageParams: any = {}; + + if (this.coursesProvider.isGetCoursesByFieldAvailable()) { + if (params.categoryid) { + page = 'CoreCoursesCategoriesPage'; + pageParams.categoryId = parseInt(params.categoryid, 10); + } else { + page = 'CoreCoursesAvailableCoursesPage'; + } + } + + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.loginHelper.redirect(page, pageParams, siteId); + } + }]; + } +} diff --git a/src/core/courses/providers/my-overview-link-handler.ts b/src/core/courses/providers/my-overview-link-handler.ts new file mode 100644 index 000000000..28905b628 --- /dev/null +++ b/src/core/courses/providers/my-overview-link-handler.ts @@ -0,0 +1,52 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreCoursesProvider } from './courses'; + +/** + * Handler to treat links to my overview. + */ +@Injectable() +export class CoreCoursesMyOverviewLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreCoursesMyOverviewLinkHandler'; + featureName = '$mmSideMenuDelegate_mmCourses'; + pattern = /\/my\/?$/; + + constructor(private coursesProvider: CoreCoursesProvider, private loginHelper: CoreLoginHelperProvider) { + super(); + } + + /** + * 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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + return [{ + action: (siteId, navCtrl?) => { + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.loginHelper.redirect('CoreCoursesMyOverviewPage', undefined, siteId); + } + }]; + } +} diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index 32c939a05..9ba824b40 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -21,6 +21,8 @@ import { CoreSitesProvider } from '../../../../providers/sites'; import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; import { CoreUtilsProvider } from '../../../../providers/utils/utils'; import { CoreLoginHelperProvider } from '../../providers/helper'; +import { CoreContentLinksDelegate } from '../../../contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '../../../contentlinks/providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; /** @@ -52,7 +54,8 @@ export class CoreLoginCredentialsPage { constructor(private navCtrl: NavController, navParams: NavParams, fb: FormBuilder, private appProvider: CoreAppProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider, - private eventsProvider: CoreEventsProvider) { + private eventsProvider: CoreEventsProvider, private contentLinksDelegate: CoreContentLinksDelegate, + private contentLinksHelper: CoreContentLinksHelperProvider) { this.siteUrl = navParams.get('siteUrl'); this.siteConfig = navParams.get('siteConfig'); @@ -203,16 +206,15 @@ export class CoreLoginCredentialsPage { if (this.urlToOpen) { // There's a content link to open. - // @todo: Implement this once content links delegate is implemented. - // return $mmContentLinksDelegate.getActionsFor(urlToOpen, undefined, username).then((actions) => { - // action = $mmContentLinksHelper.getFirstValidAction(actions); - // if (action && action.sites.length) { - // // Action should only have 1 site because we're filtering by username. - // action.action(action.sites[0]); - // } else { - // return $mmLoginHelper.goToSiteInitialPage(); - // } - // }); + return this.contentLinksDelegate.getActionsFor(this.urlToOpen, undefined, username).then((actions) => { + const action = this.contentLinksHelper.getFirstValidAction(actions); + if (action && action.sites.length) { + // Action should only have 1 site because we're filtering by username. + action.action(action.sites[0]); + } else { + return this.loginHelper.goToSiteInitialPage(); + } + }); } else { return this.loginHelper.goToSiteInitialPage(); } diff --git a/src/core/sitehome/providers/index-link-handler.ts b/src/core/sitehome/providers/index-link-handler.ts new file mode 100644 index 000000000..daacc1542 --- /dev/null +++ b/src/core/sitehome/providers/index-link-handler.ts @@ -0,0 +1,84 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreSiteHomeProvider } from './sitehome'; + +/** + * Handler to treat links to site home index. + */ +@Injectable() +export class CoreSiteHomeIndexLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreSiteHomeIndexLinkHandler'; + featureName = '$mmSideMenuDelegate_mmaFrontpage'; + pattern = /\/course\/view\.php.*([\?\&]id=\d+)/; + + constructor(private sitesProvider: CoreSitesProvider, private siteHomeProvider: CoreSiteHomeProvider, + private loginHelper: CoreLoginHelperProvider) { + super(); + } + + /** + * 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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number) : + CoreContentLinksAction[]|Promise { + return [{ + action: (siteId, navCtrl?) => { + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.loginHelper.redirect('CoreSiteHomeIndexPage', undefined, siteId); + } + }]; + } + + /** + * 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} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise { + courseId = parseInt(params.id, 10); + if (!courseId) { + return false; + } + + return this.sitesProvider.getSite(siteId).then((site) => { + if (courseId != site.getSiteHomeId()) { + // The course is not site home. + return false; + } + + return this.siteHomeProvider.isAvailable(siteId).then(() => { + return true; + }).catch(() => { + return false; + }); + }); + } +} diff --git a/src/core/sitehome/sitehome.module.ts b/src/core/sitehome/sitehome.module.ts index fc41faded..c498d2b12 100644 --- a/src/core/sitehome/sitehome.module.ts +++ b/src/core/sitehome/sitehome.module.ts @@ -15,7 +15,9 @@ import { NgModule } from '@angular/core'; import { CoreSiteHomeProvider } from './providers/sitehome'; import { CoreSiteHomeMainMenuHandler } from './providers/mainmenu-handler'; +import { CoreSiteHomeIndexLinkHandler } from './providers/index-link-handler'; import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; +import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; @NgModule({ declarations: [], @@ -23,12 +25,15 @@ import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; ], providers: [ CoreSiteHomeProvider, - CoreSiteHomeMainMenuHandler + CoreSiteHomeMainMenuHandler, + CoreSiteHomeIndexLinkHandler ], exports: [] }) export class CoreSiteHomeModule { - constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreSiteHomeMainMenuHandler) { + constructor(mainMenuDelegate: CoreMainMenuDelegate, contentLinksDelegate: CoreContentLinksDelegate, + mainMenuHandler: CoreSiteHomeMainMenuHandler, indexLinkHandler: CoreSiteHomeIndexLinkHandler) { mainMenuDelegate.registerHandler(mainMenuHandler); + contentLinksDelegate.registerHandler(indexLinkHandler); } } diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index da089bc25..d150799a9 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; -import { Platform } from 'ionic-angular'; +import { Platform, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '../providers/app'; import { CoreFilepoolProvider } from '../providers/filepool'; @@ -26,6 +26,7 @@ import { CoreUtilsProvider } from '../providers/utils/utils'; import { CoreSite } from '../classes/site'; import { CoreLinkDirective } from '../directives/link'; import { CoreExternalContentDirective } from '../directives/external-content'; +import { CoreContentLinksHelperProvider } from '../core/contentlinks/providers/helper'; /** * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective @@ -60,7 +61,8 @@ export class CoreFormatTextDirective implements OnChanges { constructor(element: ElementRef, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, private translate: TranslateService, private platform: Platform, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private loggerProvider: CoreLoggerProvider, - private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider) { + private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider, + private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController) { this.element = element.nativeElement; this.element.classList.add('opacity-hide'); // Hide contents until they're treated. this.afterRender = new EventEmitter(); @@ -274,7 +276,8 @@ export class CoreFormatTextDirective implements OnChanges { // Important: We need to look for links first because in 'img' we add new links without core-link. anchors.forEach((anchor) => { // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually. - let linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils); + let linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils, + this.contentLinksHelper, this.navCtrl); linkDir.capture = true; linkDir.ngOnInit(); diff --git a/src/directives/link.ts b/src/directives/link.ts index f95aa2624..7a50b20d8 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -13,10 +13,12 @@ // limitations under the License. import { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreSitesProvider } from '../providers/sites'; import { CoreDomUtilsProvider } from '../providers/utils/dom'; import { CoreUrlUtilsProvider } from '../providers/utils/url'; import { CoreUtilsProvider } from '../providers/utils/utils'; +import { CoreContentLinksHelperProvider } from '../core/contentlinks/providers/helper'; import { CoreConfigConstants } from '../configconstants'; /** @@ -36,7 +38,8 @@ export class CoreLinkDirective implements OnInit { protected element: HTMLElement; constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, - private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider) { + private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider, + private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController) { // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. this.element = element.nativeElement || element; } @@ -56,12 +59,11 @@ export class CoreLinkDirective implements OnInit { event.stopPropagation(); if (this.utils.isTrueOrOne(this.capture)) { - // @todo: Handle link using content links helper. - // $mmContentLinksHelper.handleLink(href).then((treated) => { - // if (!treated) { + this.contentLinksHelper.handleLink(href, undefined, this.navCtrl).then((treated) => { + if (!treated) { this.navigate(href); - // } - // }); + } + }); } else { this.navigate(href); }