MOBILE-2310 course: Implement format delegate and section view
parent
5303d3ab23
commit
c14bc38569
|
@ -48,6 +48,7 @@ import { CoreFilepoolProvider } from '../providers/filepool';
|
|||
import { CoreUpdateManagerProvider } from '../providers/update-manager';
|
||||
import { CorePluginFileDelegate } from '../providers/plugin-file-delegate';
|
||||
|
||||
// Core modules.
|
||||
import { CoreComponentsModule } from '../components/components.module';
|
||||
import { CoreEmulatorModule } from '../core/emulator/emulator.module';
|
||||
import { CoreLoginModule } from '../core/login/login.module';
|
||||
|
@ -55,6 +56,9 @@ import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module';
|
|||
import { CoreCoursesModule } from '../core/courses/courses.module';
|
||||
import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module';
|
||||
import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module';
|
||||
import { CoreCourseModule } from '../core/course/course.module';
|
||||
|
||||
// Addon modules.
|
||||
import { AddonCalendarModule } from '../addon/calendar/calendar.module';
|
||||
|
||||
// For translate loader. AoT requires an exported function for factories.
|
||||
|
@ -80,13 +84,14 @@ export function createTranslateLoader(http: HttpClient) {
|
|||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
CoreComponentsModule,
|
||||
CoreEmulatorModule,
|
||||
CoreLoginModule,
|
||||
CoreMainMenuModule,
|
||||
CoreCoursesModule,
|
||||
CoreFileUploaderModule,
|
||||
CoreSharedFilesModule,
|
||||
CoreComponentsModule,
|
||||
CoreCourseModule,
|
||||
AddonCalendarModule
|
||||
],
|
||||
bootstrap: [IonicApp],
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '../../../components/components.module';
|
||||
import { CoreDirectivesModule } from '../../../directives/directives.module';
|
||||
import { CoreCourseFormatComponent } from './format/format';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCourseFormatComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
CoreCourseFormatComponent
|
||||
]
|
||||
})
|
||||
export class CoreCourseComponentsModule {}
|
|
@ -0,0 +1,65 @@
|
|||
<!-- Default course format. -->
|
||||
<div *ngIf="!componentInstances.courseFormat">
|
||||
<!-- Course summary. -->
|
||||
<ion-list no-lines *ngIf="!componentInstances.courseSummary">
|
||||
<summary ion-item text-wrap *ngIf="course.summary">
|
||||
<core-format-text [text]="course.summary" maxHeight="60"></core-format-text>
|
||||
</summary>
|
||||
<ion-item *ngIf="course.progress !== false">
|
||||
<core-progress-bar [progress]="course.progress"></core-progress-bar>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<ng-template #courseSummary></ng-template>
|
||||
|
||||
<!-- Section selector. -->
|
||||
<ion-row *ngIf="!componentInstances.sectionSelector && displaySectionSelector && sections && sections.length">
|
||||
<ion-col col-11>
|
||||
<ion-item>
|
||||
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions">
|
||||
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
<ion-col col-1 class="text-right">
|
||||
<!-- @todo Download button. -->
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ng-template #sectionSelector></ng-template>
|
||||
|
||||
<!-- Single section. -->
|
||||
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
|
||||
<ng-container *ngIf="!componentInstances.singleSection">
|
||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
|
||||
<core-empty-box *ngIf="!selectedSection.hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box>
|
||||
</ng-container>
|
||||
<ng-template #singleSection></ng-template>
|
||||
</div>
|
||||
|
||||
<!-- Multiple sections. -->
|
||||
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
|
||||
<ng-container *ngIf="!componentInstances.allSections">
|
||||
<ng-container *ngFor="let section of sections">
|
||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #allSections></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #sectionTemplate let-section="section">
|
||||
<section ion-list *ngIf="section.hasContent">
|
||||
<!-- Title is only displayed when viewing all sections. -->
|
||||
<ion-item-divider text-wrap color="light" *ngIf="selectedSection.id == allSectionsId && section.name">
|
||||
<core-format-text [text]="section.name"></core-format-text>
|
||||
</ion-item-divider>
|
||||
|
||||
<ion-item text-wrap *ngIf="section.summary">
|
||||
<core-format-text [text]="section.summary" maxHeight="60"></core-format-text>
|
||||
</ion-item>
|
||||
|
||||
<!-- <mm-course-module ng-if="module.visibleoncoursepage !== 0" class="item item-complex" ng-repeat="module in section.modules" module="module" completion-changed="completionChanged"></mm-course-module> -->
|
||||
</section>
|
||||
</ng-template>
|
||||
|
||||
<!-- Custom course format that overrides the default one. -->
|
||||
<ng-template #courseFormat></ng-template>
|
|
@ -0,0 +1,185 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Input, OnInit, OnChanges, ViewContainerRef, ComponentFactoryResolver, ViewChild, ChangeDetectorRef,
|
||||
SimpleChange } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreLoggerProvider } from '../../../../providers/logger';
|
||||
import { CoreCourseProvider } from '../../../course/providers/course';
|
||||
import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate';
|
||||
|
||||
/**
|
||||
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
||||
*
|
||||
* The inputs of this component will be shared with the course format components. Please use CoreCourseFormatDelegate
|
||||
* to register your handler for course formats.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-course-format [course]="course" [sections]="sections"></core-course-format>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-format',
|
||||
templateUrl: 'format.html'
|
||||
})
|
||||
export class CoreCourseFormatComponent implements OnInit, OnChanges {
|
||||
@Input() course: any; // The course to render.
|
||||
@Input() sections: any[]; // List of course sections.
|
||||
|
||||
// Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
|
||||
@ViewChild('courseFormat', { read: ViewContainerRef }) set courseFormat(el: ViewContainerRef) {
|
||||
if (this.course) {
|
||||
this.createComponent('courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), el);
|
||||
} else {
|
||||
// The component hasn't been initialized yet. Store the container.
|
||||
this.componentContainers['courseFormat'] = el;
|
||||
}
|
||||
};
|
||||
@ViewChild('courseSummary', { read: ViewContainerRef }) set courseSummary(el: ViewContainerRef) {
|
||||
this.createComponent('courseSummary', this.cfDelegate.getCourseSummaryComponent(this.course), el);
|
||||
};
|
||||
@ViewChild('sectionSelector', { read: ViewContainerRef }) set sectionSelector(el: ViewContainerRef) {
|
||||
this.createComponent('sectionSelector', this.cfDelegate.getSectionSelectorComponent(this.course), el);
|
||||
};
|
||||
@ViewChild('singleSection', { read: ViewContainerRef }) set singleSection(el: ViewContainerRef) {
|
||||
this.createComponent('singleSection', this.cfDelegate.getSingleSectionComponent(this.course), el);
|
||||
};
|
||||
@ViewChild('allSections', { read: ViewContainerRef }) set allSections(el: ViewContainerRef) {
|
||||
this.createComponent('allSections', this.cfDelegate.getAllSectionsComponent(this.course), el);
|
||||
};
|
||||
|
||||
// Instances and containers of all the components that the handler could define.
|
||||
protected componentContainers: {[type: string]: ViewContainerRef} = {};
|
||||
componentInstances: {[type: string]: any} = {};
|
||||
|
||||
displaySectionSelector: boolean;
|
||||
selectedSection: any;
|
||||
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
|
||||
selectOptions: any = {};
|
||||
|
||||
protected logger;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService,
|
||||
private factoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef) {
|
||||
this.logger = logger.getInstance('CoreCourseFormatComponent');
|
||||
this.selectOptions.title = translate.instant('core.course.sections');
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.displaySectionSelector = this.cfDelegate.displaySectionSelector(this.course);
|
||||
|
||||
this.createComponent(
|
||||
'courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), this.componentContainers['courseFormat']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: {[name: string]: SimpleChange}) {
|
||||
if (!this.selectedSection && changes.sections && this.sections) {
|
||||
this.sectionChanged(this.cfDelegate.getCurrentSection(this.course, this.sections));
|
||||
}
|
||||
|
||||
if (!Object.keys(this.componentInstances).length) {
|
||||
// We haven't created any component dynamically, stop.
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the changes to the components and call ngOnChanges if it exists.
|
||||
for (let type in this.componentInstances) {
|
||||
let instance = this.componentInstances[type];
|
||||
|
||||
for (let name in changes) {
|
||||
instance[name] = changes[name].currentValue;
|
||||
}
|
||||
|
||||
if (instance.ngOnChanges) {
|
||||
instance.ngOnChanges(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a component, add it to a container and set the input data.
|
||||
*
|
||||
* @param {string} type The "type" of the component.
|
||||
* @param {any} componentClass The class of the component to create.
|
||||
* @param {ViewContainerRef} container The container to add the component to.
|
||||
* @return {boolean} Whether the component was successfully created.
|
||||
*/
|
||||
protected createComponent(type: string, componentClass: any, container: ViewContainerRef) : boolean {
|
||||
if (!componentClass || !container) {
|
||||
// No component to instantiate or container doesn't exist right now.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.componentInstances[type] && container === this.componentContainers[type]) {
|
||||
// Component already instantiated and the component hasn't been destroyed, nothing to do.
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the component and add it to the container.
|
||||
const factory = this.factoryResolver.resolveComponentFactory(componentClass),
|
||||
componentRef = container.createComponent(factory);
|
||||
|
||||
this.componentContainers[type] = container;
|
||||
this.componentInstances[type] = componentRef.instance;
|
||||
this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed.
|
||||
|
||||
// Set the Input data.
|
||||
this.componentInstances[type].course = this.course;
|
||||
this.componentInstances[type].sections = this.sections;
|
||||
|
||||
return true;
|
||||
} catch(ex) {
|
||||
this.logger.error('Error creating component', type, ex, componentClass);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when selected section changes.
|
||||
*
|
||||
* @param {any} newSection The new selected section.
|
||||
*/
|
||||
sectionChanged(newSection: any) {
|
||||
let previousValue = this.selectedSection;
|
||||
this.selectedSection = newSection;
|
||||
|
||||
// If there is a component to render the current section, update its section.
|
||||
if (this.componentInstances.singleSection) {
|
||||
this.componentInstances.singleSection.section = this.selectedSection;
|
||||
if (this.componentInstances.singleSection.ngOnChanges) {
|
||||
this.componentInstances.singleSection.ngOnChanges({
|
||||
section: new SimpleChange(previousValue, newSection, typeof previousValue != 'undefined')
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare if two sections are equal.
|
||||
*
|
||||
* @param {any} s1 First section.
|
||||
* @param {any} s2 Second section.
|
||||
* @return {boolean} Whether they're equal.
|
||||
*/
|
||||
compareSections(s1: any, s2: any) : boolean {
|
||||
return s1 && s2 ? s1.id === s2.id : s1 === s2;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CoreCourseProvider } from './providers/course';
|
||||
import { CoreCourseHelperProvider } from './providers/helper';
|
||||
import { CoreCourseFormatDelegate } from './providers/format-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
],
|
||||
providers: [
|
||||
CoreCourseProvider,
|
||||
CoreCourseHelperProvider,
|
||||
CoreCourseFormatDelegate
|
||||
],
|
||||
exports: []
|
||||
})
|
||||
export class CoreCourseModule {}
|
|
@ -18,5 +18,6 @@
|
|||
"hiddenfromstudents": "Hidden from students",
|
||||
"nocontentavailable": "No content available at the moment.",
|
||||
"overriddennotice": "Your final grade from this activity was manually adjusted.",
|
||||
"sections": "Sections",
|
||||
"useactivityonbrowser": "You can still use it using your device's web browser."
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item [priority]="900" [content]="'core.settings.enabledownloadsection' | translate" (action)="toggleDownload()" [iconAction]="'downloadSectionsIcon'"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="dataLoaded" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="dataLoaded">
|
||||
<div class="tabs tabs-striped tabs-free mm-tabs-color">
|
||||
<a ion-button class="tab-item">{{ 'core.course.contents' || translate }}</a>
|
||||
<a *ngFor="let handler of courseHandlers" ion-button class="tab-item">{{ handler.data.title || translate }}</a>
|
||||
</div>
|
||||
<core-course-format [course]="course" [sections]="sections"></core-course-format>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,35 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreCourseSectionPage } from './section';
|
||||
import { CoreComponentsModule } from '../../../../components/components.module';
|
||||
import { CoreDirectivesModule } from '../../../../directives/directives.module';
|
||||
import { CoreCourseComponentsModule } from '../../components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCourseSectionPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreCourseComponentsModule,
|
||||
IonicPageModule.forChild(CoreCourseSectionPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class CoreCourseSectionPageModule {}
|
|
@ -0,0 +1,128 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 } from '@angular/core';
|
||||
import { IonicPage, NavParams } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
|
||||
import { CoreCourseProvider } from '../../providers/course';
|
||||
import { CoreCourseHelperProvider } from '../../providers/helper';
|
||||
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
|
||||
import { CoreCoursesDelegate } from '../../../courses/providers/delegate';
|
||||
|
||||
/**
|
||||
* Page that displays the list of courses the user is enrolled in.
|
||||
*/
|
||||
@IonicPage({segment: 'core-course-section'})
|
||||
@Component({
|
||||
selector: 'page-core-course-section',
|
||||
templateUrl: 'section.html',
|
||||
})
|
||||
export class CoreCourseSectionPage {
|
||||
title: string;
|
||||
course: any;
|
||||
sections: any[];
|
||||
courseHandlers: any[];
|
||||
dataLoaded: boolean;
|
||||
|
||||
constructor(navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
|
||||
private courseFormatDelegate: CoreCourseFormatDelegate, private coursesDelegate: CoreCoursesDelegate,
|
||||
private translate: TranslateService, private courseHelper: CoreCourseHelperProvider,
|
||||
private textUtils: CoreTextUtilsProvider) {
|
||||
this.course = navParams.get('course');
|
||||
this.title = courseFormatDelegate.getCourseTitle(this.course);
|
||||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ionViewDidLoad() {
|
||||
this.loadData().finally(() => {
|
||||
this.dataLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and load all the data required for the view.
|
||||
*/
|
||||
protected loadData(refresh?: boolean) {
|
||||
let promises = [],
|
||||
promise;
|
||||
|
||||
// Get the completion status.
|
||||
if (this.course.enablecompletion === false) {
|
||||
// Completion not enabled.
|
||||
promise = Promise.resolve({});
|
||||
} else {
|
||||
promise = this.courseProvider.getActivitiesCompletionStatus(this.course.id).catch(() => {
|
||||
// It failed, don't use completion.
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
promises.push(promise.then((completionStatus) => {
|
||||
// Get all the sections.
|
||||
promises.push(this.courseProvider.getSections(this.course.id, false, true).then((sections) => {
|
||||
// Format the name of each section and check if it has content.
|
||||
this.sections = sections.map((section) => {
|
||||
this.textUtils.formatText(section.name.trim(), true, true).then((name) => {
|
||||
section.formattedName = name;
|
||||
});
|
||||
section.hasContent = this.courseHelper.sectionHasContent(section);
|
||||
return section;
|
||||
});
|
||||
|
||||
|
||||
if (this.courseFormatDelegate.canViewAllSections(this.course)) {
|
||||
// Add a fake first section (all sections).
|
||||
this.sections.unshift({
|
||||
name: this.translate.instant('core.course.allsections'),
|
||||
id: CoreCourseProvider.ALL_SECTIONS_ID
|
||||
});
|
||||
}
|
||||
}));
|
||||
}));
|
||||
|
||||
// Load the course handlers.
|
||||
promises.push(this.coursesDelegate.getHandlersToDisplay(this.course, refresh, false).then((handlers) => {
|
||||
this.courseHandlers = handlers;
|
||||
}));
|
||||
|
||||
return Promise.all(promises).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'mm.course.couldnotloadsectioncontent', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param {any} refresher Refresher.
|
||||
*/
|
||||
doRefresh(refresher: any) {
|
||||
let promises = [];
|
||||
|
||||
promises.push(this.courseProvider.invalidateSections(this.course.id));
|
||||
|
||||
// if ($scope.sections) {
|
||||
// promises.push($mmCoursePrefetchDelegate.invalidateCourseUpdates(courseId));
|
||||
// }
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.loadData(true).finally(() => {
|
||||
refresher.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -27,6 +27,8 @@ import { CoreConstants } from '../../constants';
|
|||
*/
|
||||
@Injectable()
|
||||
export class CoreCourseProvider {
|
||||
public static ALL_SECTIONS_ID = -1;
|
||||
|
||||
// Variables for database.
|
||||
protected COURSE_STATUS_TABLE = 'course_status';
|
||||
protected courseStatusTableSchema = {
|
||||
|
|
|
@ -0,0 +1,368 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Injectable } from '@angular/core';
|
||||
import { NavController } from 'ionic-angular';
|
||||
import { CoreEventsProvider } from '../../../providers/events';
|
||||
import { CoreLoggerProvider } from '../../../providers/logger';
|
||||
import { CoreSitesProvider } from '../../../providers/sites';
|
||||
import { CoreCourseProvider } from './course';
|
||||
|
||||
/**
|
||||
* Interface that all course format handlers should implement.
|
||||
*/
|
||||
export interface CoreCourseFormatHandler {
|
||||
/**
|
||||
* Name of the format. It should match the "format" returned in core_course_get_courses.
|
||||
* @type {string}
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean|Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get the title to use in course page. If not defined, course fullname.
|
||||
*
|
||||
* @param {any} course The course.
|
||||
* @return {string} Title.
|
||||
*/
|
||||
getCourseTitle?(course: any) : string;
|
||||
|
||||
/**
|
||||
* Whether it allows seeing all sections at the same time. Defaults to true.
|
||||
*
|
||||
* @param {any} course The course to check.
|
||||
* @type {boolean} Whether it can view all sections.
|
||||
*/
|
||||
canViewAllSections?(course: any) : boolean;
|
||||
|
||||
/**
|
||||
* Whether the default section selector should be displayed. Defaults to true.
|
||||
*
|
||||
* @param {any} course The course to check.
|
||||
* @type {boolean} Whether the default section selector should be displayed.
|
||||
*/
|
||||
displaySectionSelector?(course: any) : boolean;
|
||||
|
||||
/**
|
||||
* Given a list of sections, get the "current" section that should be displayed first. Defaults to first section.
|
||||
*
|
||||
* @param {any} course The course to get the title.
|
||||
* @param {any[]} sections List of sections.
|
||||
* @return {any} Current section.
|
||||
*/
|
||||
getCurrentSection?(course: any, sections: any[]) : any;
|
||||
|
||||
/**
|
||||
* Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened.
|
||||
* Implement it only if you want to create your own page to display the course. In general it's better to use the method
|
||||
* getCourseFormatComponent because it will display the course handlers at the top.
|
||||
* Your page should include the course handlers using CoreCoursesDelegate.
|
||||
*
|
||||
* @param {NavController} navCtrl The NavController instance to use.
|
||||
* @param {any} course The course to open. It should contain a "format" attribute.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
openCourse?(navCtrl: NavController, course: any) : Promise<any>;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the course format instead of using the default one.
|
||||
* Use it if you want to display a format completely different from the default one.
|
||||
* If you want to customize the default format there are several methods to customize parts of it.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getCourseFormatComponent?(course: any) : any;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the course summary inside the default course format.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getCourseSummaryComponent?(course: any): any;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the section selector inside the default course format.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getSectionSelectorComponent?(course: any): any;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display a single section. This component will only be used if the user is viewing a
|
||||
* single section. If all the sections are displayed at once then it won't be used.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getSingleSectionComponent?(course: any): any;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display all sections in a course.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getAllSectionsComponent?(course: any): any;
|
||||
};
|
||||
|
||||
/**
|
||||
* Service to interact with course formats. Provides the functions to register and interact with the addons.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CoreCourseFormatDelegate {
|
||||
protected logger;
|
||||
protected handlers: {[s: string]: CoreCourseFormatHandler} = {}; // All registered handlers.
|
||||
protected enabledHandlers: {[s: string]: CoreCourseFormatHandler} = {}; // Handlers enabled for the current site.
|
||||
protected lastUpdateHandlersStart: number;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) {
|
||||
this.logger = logger.getInstance('CoreCoursesCourseFormatDelegate');
|
||||
|
||||
eventsProvider.on(CoreEventsProvider.LOGIN, this.updateHandlers.bind(this));
|
||||
eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.updateHandlers.bind(this));
|
||||
eventsProvider.on(CoreEventsProvider.REMOTE_ADDONS_LOADED, this.updateHandlers.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether it allows seeing all sections at the same time. Defaults to true.
|
||||
*
|
||||
* @param {any} course The course to check.
|
||||
* @return {boolean} Whether it allows seeing all sections at the same time.
|
||||
*/
|
||||
canViewAllSections(course: any) : boolean {
|
||||
return this.executeFunction(course.format, 'canViewAllSections', true, [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the default section selector should be displayed. Defaults to true.
|
||||
*
|
||||
* @param {any} course The course to check.
|
||||
* @return {boolean} Whether the section selector should be displayed.
|
||||
*/
|
||||
displaySectionSelector(course: any) : boolean {
|
||||
return this.executeFunction(course.format, 'displaySectionSelector', true, [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a certain function in a course format handler. If the handler isn't found or function isn't defined,
|
||||
* return the default value.
|
||||
*
|
||||
* @param {string} format The format name.
|
||||
* @param {string} fnName Name of the function to execute.
|
||||
* @param {any} defaultValue Value to return if not found.
|
||||
* @param {any[]} params Parameters to pass to the function.
|
||||
* @return {any} Function returned value or default value.
|
||||
*/
|
||||
protected executeFunction(format: string, fnName: string, defaultValue?: any, params?: any[]) : any {
|
||||
let handler = this.enabledHandlers[format];
|
||||
if (handler && handler[fnName]) {
|
||||
return handler[fnName].apply(handler, params);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to use to display all sections in a course.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getAllSectionsComponent(course: any) : any {
|
||||
return this.executeFunction(course.format, 'getAllSectionsComponent', undefined, [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to use to display a course format.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getCourseFormatComponent(course: any) : any {
|
||||
return this.executeFunction(course.format, 'getCourseFormatComponent', undefined, [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to use to display the course summary in the default course format.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getCourseSummaryComponent(course: any) : any {
|
||||
return this.executeFunction(course.format, 'getCourseSummaryComponent', undefined, [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a course, return the title to use in the course page.
|
||||
*
|
||||
* @param {any} course The course to get the title.
|
||||
* @return {string} Course title.
|
||||
*/
|
||||
getCourseTitle(course: any) : string {
|
||||
return this.executeFunction(course.format, 'getCourseTitle', course.fullname || '', [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a course and a list of sections, return the current section that should be displayed first.
|
||||
*
|
||||
* @param {any} course The course to get the title.
|
||||
* @param {any[]} sections List of sections.
|
||||
* @return {any} Current section.
|
||||
*/
|
||||
getCurrentSection(course: any, sections: any[]) : any {
|
||||
// Calculate default section (the first one that isn't all sections).
|
||||
let defaultSection;
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
let section = sections[i];
|
||||
if (section.id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
defaultSection = section;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.executeFunction(course.format, 'getCurrentSection', defaultSection, [course, sections]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to use to display the section selector inside the default course format.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getSectionSelectorComponent(course: any) : any {
|
||||
return this.executeFunction(course.format, 'getSectionSelectorComponent', undefined, [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to use to display a single section. This component will only be used if the user is viewing
|
||||
* a single section. If all the sections are displayed at once then it won't be used.
|
||||
*
|
||||
* @param {any} course The course to render.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getSingleSectionComponent(course: any) : any {
|
||||
return this.executeFunction(course.format, 'getSingleSectionComponent', undefined, [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a time belongs to the last update handlers call.
|
||||
* This is to handle the cases where updateHandlers don't finish in the same order as they're called.
|
||||
*
|
||||
* @param {number} time Time to check.
|
||||
* @return {boolean} Whether it's the last call.
|
||||
*/
|
||||
isLastUpdateCall(time: number) : boolean {
|
||||
if (!this.lastUpdateHandlersStart) {
|
||||
return true;
|
||||
}
|
||||
return time == this.lastUpdateHandlersStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a course.
|
||||
*
|
||||
* @param {NavController} navCtrl The NavController instance to use.
|
||||
* @param {any} course The course to open. It should contain a "format" attribute.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
openCourse(navCtrl: NavController, course: any) : Promise<any> {
|
||||
if (this.enabledHandlers[course.format] && this.enabledHandlers[course.format].openCourse) {
|
||||
return this.enabledHandlers[course.format].openCourse(navCtrl, course);
|
||||
}
|
||||
return navCtrl.push('CoreCourseSectionPage', {course: course});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a handler.
|
||||
*
|
||||
* @param {CoreCourseFormatHandler} handler The handler to register.
|
||||
* @return {boolean} True if registered successfully, false otherwise.
|
||||
*/
|
||||
registerHandler(handler: CoreCourseFormatHandler) : boolean {
|
||||
if (typeof this.handlers[handler.name] !== 'undefined') {
|
||||
this.logger.log(`Addon '${handler.name}' already registered`);
|
||||
return false;
|
||||
}
|
||||
this.logger.log(`Registered addon '${handler.name}'`);
|
||||
this.handlers[handler.name] = handler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the handler for the current site.
|
||||
*
|
||||
* @param {CoreCourseFormatHandler} handler The handler to check.
|
||||
* @param {number} time Time this update process started.
|
||||
* @return {Promise<void>} Resolved when done.
|
||||
*/
|
||||
protected updateHandler(handler: CoreCourseFormatHandler, time: number) : Promise<void> {
|
||||
let promise,
|
||||
siteId = this.sitesProvider.getCurrentSiteId(),
|
||||
currentSite = this.sitesProvider.getCurrentSite();
|
||||
|
||||
if (!this.sitesProvider.isLoggedIn()) {
|
||||
promise = Promise.reject(null);
|
||||
} else if (currentSite.isFeatureDisabled('CoreCourseFormatHandler_' + handler.name)) {
|
||||
promise = Promise.resolve(false);
|
||||
} else {
|
||||
promise = Promise.resolve(handler.isEnabled());
|
||||
}
|
||||
|
||||
// Checks if the handler is enabled.
|
||||
return promise.catch(() => {
|
||||
return false;
|
||||
}).then((enabled: boolean) => {
|
||||
// Verify that this call is the last one that was started.
|
||||
// Check that site hasn't changed since the check started.
|
||||
if (this.isLastUpdateCall(time) && this.sitesProvider.getCurrentSiteId() === siteId) {
|
||||
if (enabled) {
|
||||
this.enabledHandlers[handler.name] = handler;
|
||||
} else {
|
||||
delete this.enabledHandlers[handler.name];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the handlers for the current site.
|
||||
*
|
||||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
protected updateHandlers() : Promise<any> {
|
||||
let promises = [],
|
||||
now = Date.now();
|
||||
|
||||
this.logger.debug('Updating handlers for current site.');
|
||||
|
||||
this.lastUpdateHandlersStart = now;
|
||||
|
||||
// Loop over all the handlers.
|
||||
for (let name in this.handlers) {
|
||||
promises.push(this.updateHandler(this.handlers[name], now));
|
||||
}
|
||||
|
||||
return Promise.all(promises).catch(() => {
|
||||
// Never reject.
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Injectable } from '@angular/core';
|
||||
import { CoreCourseProvider } from './course';
|
||||
|
||||
/**
|
||||
* Helper to gather some common course functions.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CoreCourseHelperProvider {
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Check if a section has content.
|
||||
*
|
||||
* @param {any} section Section to check.
|
||||
* @return {boolean} Whether the section has content.
|
||||
*/
|
||||
sectionHasContent(section: any) : boolean {
|
||||
if (section.id == CoreCourseProvider.ALL_SECTIONS_ID || section.hiddenbynumsections) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (typeof section.availabilityinfo != 'undefined' && section.availabilityinfo != '') ||
|
||||
section.summary != '' || (section.modules && section.modules.length > 0);
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { NavController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate';
|
||||
|
||||
/**
|
||||
* This component is meant to display a course for a list of courses with progress.
|
||||
|
@ -43,7 +44,8 @@ export class CoreCoursesCourseProgressComponent implements OnInit {
|
|||
};
|
||||
protected buttons;
|
||||
|
||||
constructor(private navCtrl: NavController, private translate: TranslateService) {
|
||||
constructor(private navCtrl: NavController, private translate: TranslateService,
|
||||
private courseFormatDelegate: CoreCourseFormatDelegate) {
|
||||
this.downloadText = this.translate.instant('core.course.downloadcourse');
|
||||
this.downloadingText = this.translate.instant('core.downloading');
|
||||
}
|
||||
|
@ -59,7 +61,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit {
|
|||
* Open a course.
|
||||
*/
|
||||
openCourse(course) {
|
||||
this.navCtrl.push('CoreCourseSectionPage', {course: course});
|
||||
this.courseFormatDelegate.openCourse(this.navCtrl, course);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue