MOBILE-3594 courses: Add course preview page
parent
cc6e87ea5c
commit
b96b6a98fe
|
@ -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}}"
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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>
|
|
@ -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 { }
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue