MOBILE-3594 courses: Add course preview page

main
Pau Ferrer Ocaña 2020-11-20 12:02:13 +01:00
parent cc6e87ea5c
commit b96b6a98fe
9 changed files with 811 additions and 0 deletions

View File

@ -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 <strong>at least</strong> {{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}}"
}

View File

@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular'; import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { FormsModule } from '@angular/forms';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.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 { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item';
import { CoreCoursesCourseProgressComponent } from './course-progress/course-progress'; import { CoreCoursesCourseProgressComponent } from './course-progress/course-progress';
import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/course-options-menu'; import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/course-options-menu';
import { CoreCoursesSelfEnrolPasswordComponent } from './self-enrol-password/self-enrol-password';
@NgModule({ @NgModule({
declarations: [ declarations: [
CoreCoursesCourseListItemComponent, CoreCoursesCourseListItemComponent,
CoreCoursesCourseProgressComponent, CoreCoursesCourseProgressComponent,
CoreCoursesCourseOptionsMenuComponent, CoreCoursesCourseOptionsMenuComponent,
CoreCoursesSelfEnrolPasswordComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
IonicModule, IonicModule,
FormsModule,
TranslateModule.forChild(), TranslateModule.forChild(),
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
@ -43,6 +47,7 @@ import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/cou
CoreCoursesCourseListItemComponent, CoreCoursesCourseListItemComponent,
CoreCoursesCourseProgressComponent, CoreCoursesCourseProgressComponent,
CoreCoursesCourseOptionsMenuComponent, CoreCoursesCourseOptionsMenuComponent,
CoreCoursesSelfEnrolPasswordComponent,
], ],
entryComponents: [ entryComponents: [
CoreCoursesCourseOptionsMenuComponent, CoreCoursesCourseOptionsMenuComponent,

View File

@ -0,0 +1,34 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'core.courses.selfenrolment' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="close()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<form (ngSubmit)="submitPassword($event)" #enrolPasswordForm>
<ion-item>
<core-show-password [name]="'password'">
<ion-input
class="ion-text-wrap core-ioninput-password"
name="password"
type="password"
placeholder="{{ 'core.courses.password' | translate }}"
[(ngModel)]="password"
[core-auto-focus]
[clearOnEdit]="false">
</ion-input>
</core-show-password>
</ion-item>
<div class="ion-padding">
<ion-button expand="block" [disabled]="!password" type="submit">{{ 'core.courses.enrolme' | translate }}</ion-button>
</div>
</form>
</ion-content>

View File

@ -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);
}
}

View File

@ -63,6 +63,12 @@ const routes: Routes = [
import('@features/courses/pages/my-courses/my-courses.page.module') import('@features/courses/pages/my-courses/my-courses.page.module')
.then(m => m.CoreCoursesMyCoursesPageModule), .then(m => m.CoreCoursesMyCoursesPageModule),
}, },
{
path: 'preview',
loadChildren: () =>
import('@features/courses/pages/course-preview/course-preview.page.module')
.then(m => m.CoreCoursesCoursePreviewPageModule),
},
], ],
}, },
]; ];

View File

@ -0,0 +1,120 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title><core-format-text [text]="course?.fullname" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text></ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="dataLoaded">
<ion-list *ngIf="course">
<div *ngIf="courseImageUrl" (click)="openCourse()" class="core-course-thumb">
<img [src]="courseImageUrl" core-external-content alt=""/>
</div>
<ion-item class="ion-text-wrap" (click)="openCourse()" [title]="course.fullname" [attr.details]="!avoidOpenCourse && canAccessCourse">
<ion-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
<ion-label>
<h2><core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text></h2>
<p *ngIf="course.categoryname"><core-format-text [text]="course.categoryname"
contextLevel="coursecat" [contextInstanceId]="course.categoryid"></core-format-text></p>
<p *ngIf="course.startdate">
{{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }}
<span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}</span>
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false">
<ion-label>
<core-format-text [text]="course.summary" maxHeight="120" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</ion-label>
</ion-item>
<ng-container class="ion-text-wrap" *ngIf="course.contacts && course.contacts.length">
<ion-item-divider>{{ 'core.teachers' | translate }}</ion-item-divider>
<ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link
[userId]="contact.id"
[courseId]="isEnrolled ? course.id : null"
[attr.aria-label]="'core.viewprofile' | translate">
<ion-avatar core-user-avatar
[user]="contact" slot="start"
[userId]="contact.id"
[courseId]="isEnrolled ? course.id : null">
</ion-avatar>
<ion-label>
<h2>{{contact.fullname}}</h2>
</ion-label>
</ion-item>
<ion-item-divider></ion-item-divider>
</ng-container>
<ion-item class="ion-text-wrap" *ngIf="course.customfields">
<ion-label>
<ng-container *ngFor="let field of course.customfields">
<div *ngIf="field.value"
class="core-customfield core-customfield_{{field.type}} core-customfield_{{field.shortname}}">
<span class="core-customfieldname">
<core-format-text [text]="field.name" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</span><span class="core-customfieldseparator">: </span>
<span class="core-customfieldvalue">
<core-format-text [text]="field.value" maxHeight="120" contextLevel="course"
[contextInstanceId]="course.id">
</core-format-text>
</span>
</div>
</ng-container>
</ion-label>
</ion-item>
<div *ngIf="!isEnrolled" detail="false">
<ion-item class="ion-text-wrap" *ngFor="let instance of selfEnrolInstances">
<ion-label>
<h2>{{ instance.name }}</h2>
<ion-button expand="block" class="ion-margin-top" (click)="selfEnrolClicked(instance.id)">
{{ 'core.courses.enrolme' | translate }}
</ion-button>
</ion-label>
</ion-item>
</div>
<ion-item class="ion-text-wrap" *ngIf="!isEnrolled && paypalEnabled">
<ion-label>
<h2>{{ 'core.courses.paypalaccepted' | translate }}</h2>
<p>{{ 'core.paymentinstant' | translate }}</p>
<ion-button expand="block" class="ion-margin-top" (click)="paypalEnrol()" *ngIf="isMobile">
{{ 'core.courses.sendpaymentbutton' | translate }}
</ion-button>
</ion-label>
</ion-item>
<ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled">
<ion-label><p>{{ 'core.courses.notenrollable' | translate }}</p></ion-label>
</ion-item>
<ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false"
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate">
<ion-icon *ngIf="!prefetchCourseData.status != statusDownloaded && !prefetchCourseData.loading"
[name]="prefetchCourseData.icon" slot="start">
</ion-icon>
<ion-icon *ngIf="prefetchCourseData.status == statusDownloaded && !prefetchCourseData.loading"
slot="start" [name]="prefetchCourseData.icon" color="success"
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate" role="status">
</ion-icon>
<ion-spinner *ngIf="prefetchCourseData.loading" slot="start"></ion-spinner>
<ion-label><h2>{{ 'core.course.downloadcourse' | translate }}</h2></ion-label>
</ion-item>
<ion-item (click)="openCourse()" [title]="course.fullname" *ngIf="!avoidOpenCourse && canAccessCourse">
<ion-icon name="fas-briefcase" slot="start"></ion-icon>
<ion-label><h2>{{ 'core.course.contents' | translate }}</h2></ion-label>
</ion-item>
<ion-item [href]="courseUrl" core-link [title]="course.fullname">
<ion-icon name="fas-external-link-alt" slot="start"></ion-icon>
<ion-label><h2>{{ 'core.openinbrowser' | translate }}</h2></ion-label>
</ion-item>
</ion-list>
</core-loading>
</ion-content>

View File

@ -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 { }

View File

@ -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<void> {
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<boolean> {
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<void> {
// 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<void> {
// 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<void> {
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<void> {
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<string>();
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<IonRefresher>): Promise<void> {
const promises: Promise<void>[] = [];
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<void> {
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();
}
}
}

View File

@ -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;
}
}