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 { 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,
|
||||||
|
|
|
@ -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')
|
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),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -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