forked from EVOgeek/Vmeda.Online
		
	MOBILE-3594 courses: Add course preview page
This commit is contained in:
		
							parent
							
								
									cc6e87ea5c
								
							
						
					
					
						commit
						b96b6a98fe
					
				
							
								
								
									
										36
									
								
								src/core/features/course/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/core/features/course/lang/en.json
									
									
									
									
									
										Normal 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}}" | ||||
| } | ||||
| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user