diff --git a/src/app/app.scss b/src/app/app.scss index 215cb71e7..56f3c6d57 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -42,6 +42,7 @@ // This is done for accessibility reasons when a heading is semantically incorrect. .item .item-heading { @extend h6; + margin: 0; } .mm-oauth-icon, .item.mm-oauth-icon, .list .item.mm-oauth-icon { diff --git a/src/core/courses/pages/course-preview/course-preview.html b/src/core/courses/pages/course-preview/course-preview.html new file mode 100644 index 000000000..143e07f10 --- /dev/null +++ b/src/core/courses/pages/course-preview/course-preview.html @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + +

+

{{course.categoryname}}

+

{{course.startdate * 1000 | coreFormatDate:"dfdaymonthyear"}} - {{course.enddate * 1000 | coreFormatDate:"dfdaymonthyear"}}

+
+ + + + + + +

{{ 'core.teachers' | translate }}

+

{{contact.fullname}}

+
+ +
+ +

{{ instance.name }}

+ +
+
+ +

{{ 'core.courses.paypalaccepted' | translate }}

+

{{ 'core.paymentinstant' | translate }}

+ +
+ +

{{ 'core.courses.notenrollable' | translate }}

+
+ + + +

{{ 'core.course.contents' | translate }}

+ +
+ + + + +
+
+
diff --git a/src/core/courses/pages/course-preview/course-preview.module.ts b/src/core/courses/pages/course-preview/course-preview.module.ts new file mode 100644 index 000000000..00e38e14b --- /dev/null +++ b/src/core/courses/pages/course-preview/course-preview.module.ts @@ -0,0 +1,35 @@ +// (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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreCoursesCoursePreviewPage } from './course-preview'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CorePipesModule } from '../../../../pipes/pipes.module'; + +@NgModule({ + declarations: [ + CoreCoursesCoursePreviewPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(CoreCoursesCoursePreviewPage), + TranslateModule.forChild() + ], +}) +export class CoreCoursesCoursePreviewPageModule {} diff --git a/src/core/courses/pages/course-preview/course-preview.scss b/src/core/courses/pages/course-preview/course-preview.scss new file mode 100644 index 000000000..6667a1983 --- /dev/null +++ b/src/core/courses/pages/course-preview/course-preview.scss @@ -0,0 +1,3 @@ +page-core-courses-course-preview { + +} diff --git a/src/core/courses/pages/course-preview/course-preview.ts b/src/core/courses/pages/course-preview/course-preview.ts new file mode 100644 index 000000000..2171fc0d3 --- /dev/null +++ b/src/core/courses/pages/course-preview/course-preview.ts @@ -0,0 +1,396 @@ +// (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 { Component, OnDestroy } from '@angular/core'; +import { IonicPage, NavController, NavParams, Platform, ModalController, Modal } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '../../../../providers/app'; +import { CoreEventsProvider } from '../../../../providers/events'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreCoursesProvider } from '../../providers/courses'; + +/** + * Page that allows "previewing" a course and enrolling in it if enabled and not enrolled. + */ +@IonicPage() +@Component({ + selector: 'page-core-courses-course-preview', + templateUrl: 'course-preview.html', +}) +export class CoreCoursesCoursePreviewPage implements OnDestroy { + course: any; + isEnrolled: boolean; + handlersShouldBeShown: boolean = true; + handlersLoaded: boolean; + component = 'CoreCoursesCoursePreview'; + selfEnrolInstances: any[] = []; + paypalEnabled: boolean; + dataLoaded: boolean; + prefetchCourseIcon: string; + + protected guestWSAvailable: boolean; + protected isGuestEnabled: boolean = false; + protected guestInstanceId: number; + protected enrollmentMethods: any[]; + protected waitStart = 0; + protected enrolUrl: string; + protected courseUrl: string; + protected paypalReturnUrl: string; + protected isMobile: boolean; + protected isDesktop: boolean; + protected selfEnrolModal: Modal; + protected pageDestroyed = false; + protected currentInstanceId: number; + + constructor(private navCtrl: NavController, navParams: NavParams, private sitesProvider: CoreSitesProvider, + private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, appProvider: CoreAppProvider, + private coursesProvider: CoreCoursesProvider, private platform: Platform, private modalCtrl: ModalController, + private translate: TranslateService, private eventsProvider: CoreEventsProvider) { + this.course = navParams.get('course'); + this.isMobile = appProvider.isMobile(); + this.isDesktop = appProvider.isDesktop(); + } + + /** + * View loaded. + */ + ionViewDidLoad() { + const currentSite = this.sitesProvider.getCurrentSite(), + currentSiteUrl = currentSite && currentSite.getURL(); + + this.paypalEnabled = this.course.enrollmentmethods && this.course.enrollmentmethods.indexOf('paypal') > -1; + this.guestWSAvailable = this.coursesProvider.isGuestWSAvailable(); + this.enrolUrl = this.textUtils.concatenatePaths(currentSiteUrl, 'enrol/index.php?id=' + this.course.id); + this.courseUrl = this.textUtils.concatenatePaths(currentSiteUrl, 'course/view.php?id=' + this.course.id); + this.paypalReturnUrl = this.textUtils.concatenatePaths(currentSiteUrl, 'enrol/paypal/return.php'); + + // Initialize the self enrol modal. + this.selfEnrolModal = this.modalCtrl.create('CoreCoursesSelfEnrolPasswordPage'); + this.selfEnrolModal.onDidDismiss((password: string) => { + if (typeof password != 'undefined') { + this.selfEnrolInCourse(password, this.currentInstanceId); + } + }); + + this.getCourse().finally(() => { + // @todo: Prefetch course. + }); + } + + /** + * Page destroyed. + */ + ngOnDestroy() { + this.pageDestroyed = true; + } + + /** + * Check if the user can access as guest. + * + * @return {Promise} Promise resolved if can access as guest, rejected otherwise. Resolve param indicates if + * password is required for guest access. + */ + protected canAccessAsGuest() : Promise { + if (!this.isGuestEnabled) { + return Promise.reject(null); + } + + // Search instance ID of guest enrolment method. + this.guestInstanceId = undefined; + for (let i = 0; i < this.enrollmentMethods.length; i++) { + let method = this.enrollmentMethods[i]; + if (method.type == 'guest') { + this.guestInstanceId = method.id; + break; + } + } + + if (this.guestInstanceId) { + return this.coursesProvider.getCourseGuestEnrolmentInfo(this.guestInstanceId).then((info) => { + if (!info.status) { + // Not active, reject. + return Promise.reject(null); + } + return info.passwordrequired; + }); + } + + return Promise.reject(null); + } + + /** + * Convenience function to get course. We use this to determine if a user can see the course or not. + * + * @param {boolean} refresh Whether the user is refreshing the data. + */ + protected getCourse(refresh?: boolean) : Promise { + // Get course enrolment methods. + this.selfEnrolInstances = []; + return this.coursesProvider.getCourseEnrolmentMethods(this.course.id).then((methods) => { + this.enrollmentMethods = methods; + + this.enrollmentMethods.forEach((method) => { + if (method.type === 'self') { + this.selfEnrolInstances.push(method); + } else if (this.guestWSAvailable && method.type === 'guest') { + this.isGuestEnabled = true; + } + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting enrolment data'); + }).then(() => { + // Check if user is enrolled in the course. + return this.coursesProvider.getUserCourse(this.course.id).then((course) => { + this.isEnrolled = true; + return course; + }).catch(() => { + // The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course. + this.isEnrolled = false; + return this.coursesProvider.getCourse(this.course.id); + }).then((course) => { + // Success retrieving the course, we can assume the user has permissions to view it. + this.course.fullname = course.fullname || this.course.fullname; + this.course.summary = course.summary || this.course.summary; + return this.loadCourseNavHandlers(refresh, false); + }).catch(() => { + // The user is not an admin/manager. Check if we can provide guest access to the course. + return this.canAccessAsGuest().then((passwordRequired) => { + if (!passwordRequired) { + return this.loadCourseNavHandlers(refresh, true); + } else { + return Promise.reject(null); + } + }).catch(() => { + this.course._handlers = []; + this.handlersShouldBeShown = false; + }); + }); + }).finally(() => { + this.dataLoaded = true; + }); + } + + /** + * Load course nav handlers. + * + * @param {boolean} refresh Whether the user is refreshing the data. + * @param {boolean} guest Whether it's guest access. + */ + protected loadCourseNavHandlers(refresh: boolean, guest: boolean) : Promise { + // @todo: Get the handlers to be shown. + return new Promise((resolve, reject) => { + this.course._handlers = []; + this.handlersShouldBeShown = true; + this.handlersLoaded = true; + resolve(); + }); + // return $mmCoursesDelegate.getNavHandlersToDisplay(course, refresh, guest, true).then(function(handlers) { + // course._handlers = handlers; + // $scope.handlersShouldBeShown = true; + // }).catch(() => { + + // }); + } + + /** + * Open the course. + */ + openCourse() { + if (!this.handlersShouldBeShown) { + // Course cannot be opened. + return; + } + + this.navCtrl.push('CoreCourseSectionPage', {course: this.course}); + } + + /** + * Enrol using PayPal. + */ + paypalEnrol() { + let window, + hasReturnedFromPaypal = false, + inAppLoadSubscription, + inAppFinishSubscription, + inAppExitSubscription, + appResumeSubscription, + urlLoaded = (event) => { + if (event.url.indexOf(this.paypalReturnUrl) != -1) { + hasReturnedFromPaypal = true; + } else if (event.url.indexOf(this.courseUrl) != -1 && hasReturnedFromPaypal) { + // User reached the course index page after returning from PayPal, close the InAppBrowser. + inAppClosed(); + window.close(); + } + }, + inAppClosed = () => { + // InAppBrowser closed, refresh data. + unsubscribeAll(); + + if (!this.dataLoaded) { + return; + } + this.dataLoaded = false; + this.refreshData(); + }, + unsubscribeAll = () => { + inAppLoadSubscription && inAppLoadSubscription.unsubscribe(); + inAppFinishSubscription && inAppFinishSubscription.unsubscribe(); + inAppExitSubscription && inAppExitSubscription.unsubscribe(); + appResumeSubscription && appResumeSubscription.unsubscribe(); + }; + + // Open the enrolment page in InAppBrowser. + this.sitesProvider.getCurrentSite().openInAppWithAutoLogin(this.enrolUrl).then((w) => { + window = w; + + if (this.isDesktop || this.isMobile) { + // Observe loaded pages in the InAppBrowser to check if the enrol process has ended. + inAppLoadSubscription = window.on('loadstart').subscribe(urlLoaded); + // Observe window closed. + inAppExitSubscription = window.on('exit').subscribe(inAppClosed); + } + + if (this.isDesktop) { + // In desktop, also observe stop loading since some pages don't throw the loadstart event. + inAppFinishSubscription = window.on('loadstop').subscribe(urlLoaded); + + // Since the user can switch windows, reload the data if he comes back to the app. + appResumeSubscription = this.platform.resume.subscribe(() => { + if (!this.dataLoaded) { + return; + } + this.dataLoaded = false; + this.refreshData(); + }); + } + }); + } + + /** + * User clicked in a self enrol button. + * + * @param {number} instanceId The instance ID of the enrolment method. + */ + selfEnrolClicked(instanceId: number) { + this.domUtils.showConfirm(this.translate.instant('core.courses.confirmselfenrol')).then(() => { + this.selfEnrolInCourse('', instanceId); + }).catch(() => { + // User cancelled. + }); + } + + /** + * Self enrol in a course. + * + * @param {string} password Password to use. + * @param {number} instanceId The instance ID. + * @return {Promise} Promise resolved when self enrolled. + */ + selfEnrolInCourse(password: string, instanceId: number) : Promise { + let modal = this.domUtils.showModalLoading('core.loading', true); + + return this.coursesProvider.selfEnrol(this.course.id, password, instanceId).then(() => { + // Close modal and refresh data. + this.isEnrolled = true; + this.dataLoaded = false; + + // Sometimes the list of enrolled courses takes a while to be updated. Wait for it. + this.waitForEnrolled(true).then(() => { + this.refreshData().finally(() => { + // My courses have been updated, trigger event. + this.eventsProvider.trigger( + CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {siteId: this.sitesProvider.getCurrentSiteId()}); + }); + }); + }).catch((error) => { + if (error && error.code === CoreCoursesProvider.ENROL_INVALID_KEY) { + // Invalid password, show the modal to enter the password. + this.selfEnrolModal.present(); + this.currentInstanceId = instanceId; + + if (!password) { + // No password entered, don't show error. + return; + } + } + + this.domUtils.showErrorModalDefault(error, 'core.courses.errorselfenrol', true); + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] The refresher if this was triggered by a Pull To Refresh. + */ + refreshData(refresher?: any) : Promise { + let promises = []; + + promises.push(this.coursesProvider.invalidateUserCourses()); + promises.push(this.coursesProvider.invalidateCourse(this.course.id)); + promises.push(this.coursesProvider.invalidateCourseEnrolmentMethods(this.course.id)); + // promises.push($mmCoursesDelegate.clearAndInvalidateCoursesOptions(course.id)); + if (this.guestInstanceId) { + promises.push(this.coursesProvider.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId)); + } + + return Promise.all(promises).finally(() => { + return this.getCourse(true); + }).finally(() => { + if (refresher) { + refresher.complete(); + } + }); + } + + /** + * Wait for the user to be enrolled in the course. + * + * @param {boolean} first If it's the first call (true) or it's a recursive call (false). + */ + protected waitForEnrolled(first?: boolean) { + 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(this.course.id); + }).catch(() => { + // Not enrolled, wait a bit and try again. + if (this.pageDestroyed || (Date.now() - this.waitStart > 60000)) { + // Max time reached or the user left the view, stop. + return; + } + + return new Promise((resolve, reject) => { + setTimeout(() => { + if (!this.pageDestroyed) { + // Wait again. + this.waitForEnrolled().then(resolve); + } else { + resolve(); + } + }, 5000); + }); + }); + } +} diff --git a/src/core/courses/pages/my-courses/my-courses.ts b/src/core/courses/pages/my-courses/my-courses.ts index 2b87258e8..54ccbb318 100644 --- a/src/core/courses/pages/my-courses/my-courses.ts +++ b/src/core/courses/pages/my-courses/my-courses.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; import { CoreEventsProvider } from '../../../../providers/events'; import { CoreSitesProvider } from '../../../../providers/sites'; @@ -27,7 +27,7 @@ import { CoreCoursesProvider } from '../../providers/courses'; selector: 'page-core-courses-my-courses', templateUrl: 'my-courses.html', }) -export class CoreCoursesMyCoursesPage { +export class CoreCoursesMyCoursesPage implements OnDestroy { courses: any[]; filteredCourses: any[]; searchEnabled: boolean; diff --git a/src/core/courses/pages/self-enrol-password/self-enrol-password.html b/src/core/courses/pages/self-enrol-password/self-enrol-password.html new file mode 100644 index 000000000..b9161587b --- /dev/null +++ b/src/core/courses/pages/self-enrol-password/self-enrol-password.html @@ -0,0 +1,23 @@ + + + {{ 'core.courses.selfenrolment' | translate }} + + + + + + + +
+ + + + + + + + +
+
diff --git a/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts b/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts new file mode 100644 index 000000000..80a4f5183 --- /dev/null +++ b/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts @@ -0,0 +1,33 @@ +// (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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { CoreCoursesSelfEnrolPasswordPage } from './self-enrol-password'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; + +@NgModule({ + declarations: [ + CoreCoursesSelfEnrolPasswordPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreCoursesSelfEnrolPasswordPage), + TranslateModule.forChild(), + ] +}) +export class CoreCoursesSelfEnrolPasswordPageModule {} diff --git a/src/core/courses/pages/self-enrol-password/self-enrol-password.ts b/src/core/courses/pages/self-enrol-password/self-enrol-password.ts new file mode 100644 index 000000000..dcdc404fe --- /dev/null +++ b/src/core/courses/pages/self-enrol-password/self-enrol-password.ts @@ -0,0 +1,44 @@ +// (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 { Component } from '@angular/core'; +import { IonicPage, ViewController } from 'ionic-angular'; + +/** + * Page that displays a form to enter a password to self enrol in a course. + */ +@IonicPage() +@Component({ + selector: 'page-core-courses-self-enrol-password', + templateUrl: 'self-enrol-password.html', +}) +export class CoreCoursesSelfEnrolPasswordPage { + constructor(private viewCtrl: ViewController) {} + + /** + * Close help modal. + */ + close() : void { + this.viewCtrl.dismiss(); + } + + /** + * Submit password. + * + * @param {string} password Password to submit. + */ + submitPassword(password: string) { + this.viewCtrl.dismiss(password); + } +} \ No newline at end of file diff --git a/src/core/courses/providers/courses.ts b/src/core/courses/providers/courses.ts index 655f292b7..d3461af75 100644 --- a/src/core/courses/providers/courses.ts +++ b/src/core/courses/providers/courses.ts @@ -24,6 +24,7 @@ import { CoreSite } from '../../../classes/site'; export class CoreCoursesProvider { public static SEARCH_PER_PAGE = 20; public static ENROL_INVALID_KEY = 'CoreCoursesEnrolInvalidKey'; + public static EVENT_MY_COURSES_UPDATED = 'courses_my_courses_updated'; protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) {