From b96b6a98fe678710e1b889025c50d9b82515a8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 20 Nov 2020 12:02:13 +0100 Subject: [PATCH] MOBILE-3594 courses: Add course preview page --- src/core/features/course/lang/en.json | 36 ++ .../courses/components/components.module.ts | 5 + .../self-enrol-password.html | 34 ++ .../self-enrol-password.ts | 63 +++ src/core/features/courses/courses.module.ts | 6 + .../pages/course-preview/course-preview.html | 120 +++++ .../course-preview.page.module.ts | 51 ++ .../course-preview/course-preview.page.ts | 475 ++++++++++++++++++ .../pages/course-preview/course-preview.scss | 21 + 9 files changed, 811 insertions(+) create mode 100644 src/core/features/course/lang/en.json create mode 100644 src/core/features/courses/components/self-enrol-password/self-enrol-password.html create mode 100644 src/core/features/courses/components/self-enrol-password/self-enrol-password.ts create mode 100644 src/core/features/courses/pages/course-preview/course-preview.html create mode 100644 src/core/features/courses/pages/course-preview/course-preview.page.module.ts create mode 100644 src/core/features/courses/pages/course-preview/course-preview.page.ts create mode 100644 src/core/features/courses/pages/course-preview/course-preview.scss diff --git a/src/core/features/course/lang/en.json b/src/core/features/course/lang/en.json new file mode 100644 index 000000000..2a74a13a0 --- /dev/null +++ b/src/core/features/course/lang/en.json @@ -0,0 +1,36 @@ +{ + "activitydisabled": "Your organisation has disabled this activity in the mobile app.", + "activitynotyetviewableremoteaddon": "Your organisation installed a plugin that is not yet supported.", + "activitynotyetviewablesiteupgradeneeded": "Your organisation's Moodle installation needs to be updated.", + "allsections": "All sections", + "askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.", + "availablespace": " You currently have about {{available}} free space.", + "cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.", + "confirmdeletemodulefiles": "Are you sure you want to delete these files?", + "confirmdeletestoreddata": "Are you sure you want to delete the stored data?", + "confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?", + "confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?", + "confirmdownloadzerosize": "You are about to start downloading.{{availableSpace}} Are you sure you want to continue?", + "confirmpartialdownloadsize": "You are about to download at least {{size}}.{{availableSpace}} Are you sure you want to continue?", + "confirmlimiteddownload": "You are not currently connected to Wi-Fi. ", + "contents": "Contents", + "couldnotloadsectioncontent": "Could not load the section content. Please try again later.", + "couldnotloadsections": "Could not load the sections. Please try again later.", + "coursesummary": "Course summary", + "downloadcourse": "Download course", + "errordownloadingcourse": "Error downloading course.", + "errordownloadingsection": "Error downloading section.", + "errorgetmodule": "Error getting activity data.", + "hiddenfromstudents": "Hidden from students", + "hiddenoncoursepage": "Available but not shown on course page", + "insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.", + "insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.", + "manualcompletionnotsynced": "Manual completion not synchronised.", + "nocontentavailable": "No content available at the moment.", + "overriddennotice": "Your final grade from this activity was manually adjusted.", + "refreshcourse": "Refresh course", + "sections": "Sections", + "useactivityonbrowser": "You can still use it using your device's web browser.", + "warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", + "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" +} \ No newline at end of file diff --git a/src/core/features/courses/components/components.module.ts b/src/core/features/courses/components/components.module.ts index 6a74f8671..6b9eae348 100644 --- a/src/core/features/courses/components/components.module.ts +++ b/src/core/features/courses/components/components.module.ts @@ -16,6 +16,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IonicModule } from '@ionic/angular'; import { TranslateModule } from '@ngx-translate/core'; +import { FormsModule } from '@angular/forms'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; @@ -24,16 +25,19 @@ import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item'; import { CoreCoursesCourseProgressComponent } from './course-progress/course-progress'; import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/course-options-menu'; +import { CoreCoursesSelfEnrolPasswordComponent } from './self-enrol-password/self-enrol-password'; @NgModule({ declarations: [ CoreCoursesCourseListItemComponent, CoreCoursesCourseProgressComponent, CoreCoursesCourseOptionsMenuComponent, + CoreCoursesSelfEnrolPasswordComponent, ], imports: [ CommonModule, IonicModule, + FormsModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, @@ -43,6 +47,7 @@ import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/cou CoreCoursesCourseListItemComponent, CoreCoursesCourseProgressComponent, CoreCoursesCourseOptionsMenuComponent, + CoreCoursesSelfEnrolPasswordComponent, ], entryComponents: [ CoreCoursesCourseOptionsMenuComponent, diff --git a/src/core/features/courses/components/self-enrol-password/self-enrol-password.html b/src/core/features/courses/components/self-enrol-password/self-enrol-password.html new file mode 100644 index 000000000..d3acbe66a --- /dev/null +++ b/src/core/features/courses/components/self-enrol-password/self-enrol-password.html @@ -0,0 +1,34 @@ + + + + + + {{ 'core.courses.selfenrolment' | translate }} + + + + + + + + + +
+ + + + + + +
+ {{ 'core.courses.enrolme' | translate }} +
+
+
diff --git a/src/core/features/courses/components/self-enrol-password/self-enrol-password.ts b/src/core/features/courses/components/self-enrol-password/self-enrol-password.ts new file mode 100644 index 000000000..d44874761 --- /dev/null +++ b/src/core/features/courses/components/self-enrol-password/self-enrol-password.ts @@ -0,0 +1,63 @@ +// (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 { Component, ViewChild, ElementRef } from '@angular/core'; +import { ModalController, NavParams } from '@ionic/angular'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; + +/** + * Modal that displays a form to enter a password to self enrol in a course. + */ +@Component({ + selector: 'page-core-courses-self-enrol-password', + templateUrl: 'self-enrol-password.html', +}) +export class CoreCoursesSelfEnrolPasswordComponent { + + @ViewChild('enrolPasswordForm') formElement!: ElementRef; + password = ''; + + constructor( + protected modalCtrl: ModalController, + navParams: NavParams, + ) { + this.password = navParams.get('password') || ''; + } + + /** + * Close help modal. + */ + close(): void { + CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId()); + + this.modalCtrl.dismiss(); + } + + /** + * Submit password. + * + * @param e Event. + * @param password Password to submit. + */ + submitPassword(e: Event): void { + e.preventDefault(); + e.stopPropagation(); + + CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, false, CoreSites.instance.getCurrentSiteId()); + + this.modalCtrl.dismiss(this.password); + } + +} diff --git a/src/core/features/courses/courses.module.ts b/src/core/features/courses/courses.module.ts index 8845e88b5..81d147123 100644 --- a/src/core/features/courses/courses.module.ts +++ b/src/core/features/courses/courses.module.ts @@ -63,6 +63,12 @@ const routes: Routes = [ import('@features/courses/pages/my-courses/my-courses.page.module') .then(m => m.CoreCoursesMyCoursesPageModule), }, + { + path: 'preview', + loadChildren: () => + import('@features/courses/pages/course-preview/course-preview.page.module') + .then(m => m.CoreCoursesCoursePreviewPageModule), + }, ], }, ]; diff --git a/src/core/features/courses/pages/course-preview/course-preview.html b/src/core/features/courses/pages/course-preview/course-preview.html new file mode 100644 index 000000000..b11a999d8 --- /dev/null +++ b/src/core/features/courses/pages/course-preview/course-preview.html @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + +
+ +
+ + + +

+

+

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

+
+
+ + + + + + + + + + {{ 'core.teachers' | translate }} + + + + +

{{contact.fullname}}

+
+
+ +
+ + + + +
+ + + + : + + + + +
+
+
+
+ +
+ + +

{{ instance.name }}

+ + {{ 'core.courses.enrolme' | translate }} + +
+
+
+ + +

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

+

{{ 'core.paymentinstant' | translate }}

+ + {{ 'core.courses.sendpaymentbutton' | translate }} + +
+
+ +

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

+
+ + + + + + +

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

+
+ + +

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

+
+ + +

{{ 'core.openinbrowser' | translate }}

+
+
+
+
diff --git a/src/core/features/courses/pages/course-preview/course-preview.page.module.ts b/src/core/features/courses/pages/course-preview/course-preview.page.module.ts new file mode 100644 index 000000000..00f9eaa9f --- /dev/null +++ b/src/core/features/courses/pages/course-preview/course-preview.page.module.ts @@ -0,0 +1,51 @@ +// (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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; + +import { CoreCoursesCoursePreviewPage } from './course-preview.page'; +import { CoreCoursesComponentsModule } from '../../components/components.module'; + +const routes: Routes = [ + { + path: '', + component: CoreCoursesCoursePreviewPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + CoreCoursesComponentsModule, + ], + declarations: [ + CoreCoursesCoursePreviewPage, + ], + exports: [RouterModule], +}) +export class CoreCoursesCoursePreviewPageModule { } diff --git a/src/core/features/courses/pages/course-preview/course-preview.page.ts b/src/core/features/courses/pages/course-preview/course-preview.page.ts new file mode 100644 index 000000000..623a955b9 --- /dev/null +++ b/src/core/features/courses/pages/course-preview/course-preview.page.ts @@ -0,0 +1,475 @@ +// (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 { Component, OnDestroy, NgZone, OnInit } from '@angular/core'; +import { ModalController, IonRefresher, NavController } from '@ionic/angular'; +import { CoreApp } from '@services/app'; +import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { + CoreCourseEnrolmentMethod, + CoreCourseGetCoursesData, + CoreCourses, + CoreCourseSearchedData, + CoreCoursesProvider, + CoreEnrolledCourseData, +} from '@features/courses/services/courses'; +// import { CoreCourseOptionsDelegate } from '@features/course/services/options-delegate'; +import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; +import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course.helper'; +import { Translate } from '@singletons/core.singletons'; +import { ActivatedRoute } from '@angular/router'; +import { CoreConstants } from '@/core/constants'; +import { CoreCoursesSelfEnrolPasswordComponent } from '../../components/self-enrol-password/self-enrol-password'; + +/** + * Page that allows "previewing" a course and enrolling in it if enabled and not enrolled. + */ +@Component({ + selector: 'page-core-courses-course-preview', + templateUrl: 'course-preview.html', + styleUrls: ['course-preview.scss'], +}) +export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy { + + course?: CoreCourseSearchedData; + isEnrolled = false; + canAccessCourse = true; + selfEnrolInstances: CoreCourseEnrolmentMethod[] = []; + paypalEnabled = false; + dataLoaded = false; + avoidOpenCourse = false; + prefetchCourseData: CorePrefetchStatusInfo = { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + }; + + statusDownloaded = CoreConstants.DOWNLOADED; + + downloadCourseEnabled: boolean; + courseUrl = ''; + courseImageUrl?: string; + + protected isGuestEnabled = false; + protected guestInstanceId?: number; + protected enrolmentMethods: CoreCourseEnrolmentMethod[] = []; + protected waitStart = 0; + protected enrolUrl = ''; + protected paypalReturnUrl = ''; + protected isMobile: boolean; + protected pageDestroyed = false; + protected courseStatusObserver?: CoreEventObserver; + + constructor( + protected modalCtrl: ModalController, + // protected courseOptionsDelegate: CoreCourseOptionsDelegate, + protected zone: NgZone, + protected route: ActivatedRoute, + protected navCtrl: NavController, + ) { + this.isMobile = CoreApp.instance.isMobile(); + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + + if (this.downloadCourseEnabled) { + // Listen for status change in course. + this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data: CoreEventCourseStatusChanged) => { + if (data.courseId == this.course!.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { + this.updateCourseStatus(data.status); + } + }, CoreSites.instance.getCurrentSiteId()); + } + } + + /** + * View loaded. + */ + async ngOnInit(): Promise { + const navParams = this.route.snapshot.queryParams; + this.course = navParams['course']; + this.avoidOpenCourse = !!navParams['avoidOpenCourse']; + + if (!this.course) { + this.navCtrl.back(); + + return; + } + + const currentSite = CoreSites.instance.getCurrentSite(); + const currentSiteUrl = currentSite && currentSite.getURL(); + + this.paypalEnabled = this.course!.enrollmentmethods?.indexOf('paypal') > -1; + this.enrolUrl = CoreTextUtils.instance.concatenatePaths(currentSiteUrl!, 'enrol/index.php?id=' + this.course!.id); + this.courseUrl = CoreTextUtils.instance.concatenatePaths(currentSiteUrl!, 'course/view.php?id=' + this.course!.id); + this.paypalReturnUrl = CoreTextUtils.instance.concatenatePaths(currentSiteUrl!, 'enrol/paypal/return.php'); + if (this.course.overviewfiles.length > 0) { + this.courseImageUrl = this.course.overviewfiles[0].fileurl; + } + + try { + await this.getCourse(); + } finally { + if (this.downloadCourseEnabled) { + + // Determine course prefetch icon. + this.prefetchCourseData = await CoreCourseHelper.instance.getCourseStatusIconAndTitle(this.course!.id); + + if (this.prefetchCourseData.loading) { + // Course is being downloaded. Get the download promise. + const promise = CoreCourseHelper.instance.getCourseDownloadPromise(this.course!.id); + if (promise) { + // There is a download promise. If it fails, show an error. + promise.catch((error) => { + if (!this.pageDestroyed) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + } + }); + } else { + // No download, this probably means that the app was closed while downloading. Set previous status. + CoreCourse.instance.setCoursePreviousStatus(this.course!.id); + } + } + } + } + } + + /** + * Check if the user can access as guest. + * + * @return Promise resolved if can access as guest, rejected otherwise. Resolve param indicates if + * password is required for guest access. + */ + protected async canAccessAsGuest(): Promise { + if (!this.isGuestEnabled) { + throw Error('Guest access is not enabled.'); + } + + // Search instance ID of guest enrolment method. + const method = this.enrolmentMethods.find((method) => method.type == 'guest'); + this.guestInstanceId = method?.id; + + if (this.guestInstanceId) { + const info = await CoreCourses.instance.getCourseGuestEnrolmentInfo(this.guestInstanceId); + if (!info.status) { + // Not active, reject. + throw Error('Guest access is not enabled.'); + } + + return info.passwordrequired; + } + + throw Error('Guest enrollment method not found.'); + } + + /** + * Convenience function to get course. We use this to determine if a user can see the course or not. + */ + protected async getCourse(): Promise { + // Get course enrolment methods. + this.selfEnrolInstances = []; + + try { + this.enrolmentMethods = await CoreCourses.instance.getCourseEnrolmentMethods(this.course!.id); + + this.enrolmentMethods.forEach((method) => { + if (method.type === 'self') { + this.selfEnrolInstances.push(method); + } else if (method.type === 'guest') { + this.isGuestEnabled = true; + } + }); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting enrolment data'); + } + + try { + let course: CoreEnrolledCourseData | CoreCourseGetCoursesData; + + // Check if user is enrolled in the course. + try { + course = await CoreCourses.instance.getUserCourse(this.course!.id); + this.isEnrolled = true; + } 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; + + course = await CoreCourses.instance.getCourse(this.course!.id); + } + + // 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; + this.canAccessCourse = true; + } catch { + // The user is not an admin/manager. Check if we can provide guest access to the course. + try { + this.canAccessCourse = !(await this.canAccessAsGuest()); + } catch { + this.canAccessCourse = false; + } + } + + if (!CoreSites.instance.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) { + try { + const available = await CoreCourses.instance.isGetCoursesByFieldAvailableInSite(); + if (available) { + const course = await CoreCourses.instance.getCourseByField('id', this.course!.id); + + this.course!.customfields = course.customfields; + } + } catch { + // Ignore errors. + } + } + + this.dataLoaded = true; + } + + /** + * Open the course. + */ + openCourse(): void { + if (!this.canAccessCourse || this.avoidOpenCourse) { + // Course cannot be opened or we are avoiding opening because we accessed from inside a course. + return; + } + + CoreCourseHelper.instance.openCourse(this.course!); + } + + /** + * Enrol using PayPal. + */ + async paypalEnrol(): Promise { + // We cannot control browser in browser. + if (!this.isMobile || !CoreSites.instance.getCurrentSite()) { + return; + } + + let hasReturnedFromPaypal = false; + + const urlLoaded = (event: InAppBrowserEvent): void => { + 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(); + } + }; + const inAppClosed = (): void => { + // InAppBrowser closed, refresh data. + unsubscribeAll(); + + if (!this.dataLoaded) { + return; + } + this.dataLoaded = false; + this.refreshData(); + }; + const unsubscribeAll = (): void => { + inAppLoadSubscription?.unsubscribe(); + inAppExitSubscription?.unsubscribe(); + }; + + // Open the enrolment page in InAppBrowser. + const window = await CoreSites.instance.getCurrentSite()!.openInAppWithAutoLogin(this.enrolUrl); + + // Observe loaded pages in the InAppBrowser to check if the enrol process has ended. + const inAppLoadSubscription = window.on('loadstart').subscribe((event) => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + this.zone.run(() => urlLoaded(event)); + }); + // Observe window closed. + const inAppExitSubscription = window.on('exit').subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + this.zone.run(inAppClosed); + }); + } + + /** + * User clicked in a self enrol button. + * + * @param instanceId The instance ID of the enrolment method. + */ + async selfEnrolClicked(instanceId: number): Promise { + try { + await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.courses.confirmselfenrol')); + + this.selfEnrolInCourse('', instanceId); + } catch { + // User cancelled. + } + } + + /** + * Self enrol in a course. + * + * @param password Password to use. + * @param instanceId The instance ID. + * @return Promise resolved when self enrolled. + */ + async selfEnrolInCourse(password: string, instanceId: number): Promise { + const modal = await CoreDomUtils.instance.showModalLoading('core.loading', true); + + try { + await CoreCourses.instance.selfEnrol(this.course!.id, password, instanceId); + + // 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. + await this.waitForEnrolled(true); + + this.refreshData().finally(() => { + // My courses have been updated, trigger event. + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + courseId: this.course!.id, + course: this.course, + action: CoreCoursesProvider.ACTION_ENROL, + }, CoreSites.instance.getCurrentSiteId()); + }); + + modal?.dismiss(); + } catch (error) { + modal?.dismiss(); + + if (error && error.errorcode === CoreCoursesProvider.ENROL_INVALID_KEY) { + // Initialize the self enrol modal. + const selfEnrolModal = await this.modalCtrl.create( + { + component: CoreCoursesSelfEnrolPasswordComponent, + componentProps: { password }, + }, + ); + + // Invalid password, show the modal to enter the password. + await selfEnrolModal.present(); + + const data = await selfEnrolModal.onDidDismiss(); + if (typeof data?.data != 'undefined') { + this.selfEnrolInCourse(data.data, instanceId); + + return; + } + + if (!password) { + // No password entered, don't show error. + return; + } + } + + CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorselfenrol', true); + } + } + + /** + * Refresh the data. + * + * @param refresher The refresher if this was triggered by a Pull To Refresh. + */ + async refreshData(refresher?: CustomEvent): Promise { + const promises: Promise[] = []; + + promises.push(CoreCourses.instance.invalidateUserCourses()); + promises.push(CoreCourses.instance.invalidateCourse(this.course!.id)); + promises.push(CoreCourses.instance.invalidateCourseEnrolmentMethods(this.course!.id)); + // @todo promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions(this.course!.id)); + if (CoreSites.instance.getCurrentSite() && !CoreSites.instance.getCurrentSite()!.isVersionGreaterEqualThan('3.7')) { + promises.push(CoreCourses.instance.invalidateCoursesByField('id', this.course!.id)); + } + if (this.guestInstanceId) { + promises.push(CoreCourses.instance.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId)); + } + + await Promise.all(promises).finally(() => this.getCourse()).finally(() => { + refresher?.detail.complete(); + }); + } + + /** + * Update the course status icon and title. + * + * @param status Status to show. + */ + protected updateCourseStatus(status: string): void { + this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status); + } + + /** + * Wait for the user to be enrolled in the course. + * + * @param first If it's the first call (true) or it's a recursive call (false). + * @return Promise resolved when enrolled or timeout. + */ + protected async waitForEnrolled(first?: boolean): Promise { + if (first) { + this.waitStart = Date.now(); + } + + // Check if user is enrolled in the course. + try { + CoreCourses.instance.invalidateUserCourses(); + } catch { + // Ignore errors. + } + + try { + CoreCourses.instance.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): void => { + setTimeout(async () => { + if (!this.pageDestroyed) { + // Wait again. + await this.waitForEnrolled(); + } + resolve(); + }, 5000); + }); + } + } + + /** + * Prefetch the course. + */ + prefetchCourse(): void { + /* @todo CoreCourseHelper.instance.confirmAndPrefetchCourse(this.prefetchCourseData, this.course).catch((error) => { + if (!this.pageDestroyed) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + } + });*/ + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.pageDestroyed = true; + + if (this.courseStatusObserver) { + this.courseStatusObserver.off(); + } + } + +} diff --git a/src/core/features/courses/pages/course-preview/course-preview.scss b/src/core/features/courses/pages/course-preview/course-preview.scss new file mode 100644 index 000000000..02d06fcd7 --- /dev/null +++ b/src/core/features/courses/pages/course-preview/course-preview.scss @@ -0,0 +1,21 @@ +:host { + .core-course-thumb { + height: 150px; + width: 100%; + overflow: hidden; + cursor: pointer; + pointer-events: auto; + position: relative; + + img { + position: absolute; + top: 0; + bottom: 0; + margin: auto; + width: 100%; + } + } + .core-customfieldvalue core-format-text { + display: inline; + } +}