MOBILE-2310 course: Implement singleactivity format

main
Dani Palou 2018-01-18 09:50:22 +01:00
parent 398e7c2bf4
commit 514e9bf132
17 changed files with 395 additions and 48 deletions

View File

@ -22,13 +22,15 @@ import { CoreCourseFormatComponent } from './format/format';
import { CoreCourseModuleComponent } from './module/module';
import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion';
import { CoreCourseModuleDescriptionComponent } from './module-description/module-description';
import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
@NgModule({
declarations: [
CoreCourseFormatComponent,
CoreCourseModuleComponent,
CoreCourseModuleCompletionComponent,
CoreCourseModuleDescriptionComponent
CoreCourseModuleDescriptionComponent,
CoreCourseUnsupportedModuleComponent
],
imports: [
CommonModule,
@ -43,7 +45,11 @@ import { CoreCourseModuleDescriptionComponent } from './module-description/modul
CoreCourseFormatComponent,
CoreCourseModuleComponent,
CoreCourseModuleCompletionComponent,
CoreCourseModuleDescriptionComponent
CoreCourseModuleDescriptionComponent,
CoreCourseUnsupportedModuleComponent
],
entryComponents: [
CoreCourseUnsupportedModuleComponent
]
})
export class CoreCourseComponentsModule {}

View File

@ -210,16 +210,17 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
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;
this.componentInstances[type].downloadEnabled = this.downloadEnabled;
this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed.
return true;
} catch(ex) {
this.logger.error('Error creating component', type, ex, componentClass);
this.logger.error('Error creating component', type, ex);
return false;
}
}

View File

@ -0,0 +1,18 @@
<div padding>
<core-course-module-description [description]="module.description"></core-course-module-description>
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
<h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2>
<p class="core-big" *ngIf="isDisabledInSite">{{ 'core.course.activitydisabled' | translate }}</p>
<p class="core-big" *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.course.activitynotyetviewablesiteupgradeneeded' | translate }}</p>
<p class="core-big" *ngIf="!isDisabledInSite && !isSupportedByTheApp">{{ 'core.course.activitynotyetviewableremoteaddon' | translate }}</p>
<p *ngIf="isDisabledInSite || !isSupportedByTheApp"><strong>{{ 'core.course.askadmintosupport' | translate }}</strong></p>
<div *ngIf="module.url">
<p><strong>{{ 'core.course.useactivityonbrowser' | translate }}</strong></p>
<a ion-button block icon-end [href]="module.url" core-link>
{{ 'core.openinbrowser' | translate }}
<ion-icon name="open"></ion-icon>
</a>
</div>
</div>

View File

@ -0,0 +1,48 @@
// (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 } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
import { CoreCourseProvider } from '../../providers/course';
import { CoreCourseModuleDelegate } from '../../providers/module-delegate';
/**
* Component that displays info about an unsupported module.
*/
@Component({
selector: 'core-course-unsupported-module',
templateUrl: 'unsupported-module.html',
})
export class CoreCourseUnsupportedModuleComponent implements OnInit {
@Input() course: any; // The course to module belongs to.
@Input() module: any; // The module to render.
isDisabledInSite: boolean;
isSupportedByTheApp: boolean;
moduleName: string;
constructor(navParams: NavParams, private translate: TranslateService, private textUtils: CoreTextUtilsProvider,
private courseProvider: CoreCourseProvider, private moduleDelegate: CoreCourseModuleDelegate) {}
/**
* Component being initialized.
*/
ngOnInit() {
this.isDisabledInSite = this.moduleDelegate.isModuleDisabledInSite(this.module.modname);
this.isSupportedByTheApp = this.moduleDelegate.hasHandler(this.module.modname);
this.moduleName = this.courseProvider.translateModuleName(this.module.modname);
}
}

View File

@ -19,12 +19,14 @@ import { CoreCourseFormatDelegate } from './providers/format-delegate';
import { CoreCourseModuleDelegate } from './providers/module-delegate';
import { CoreCourseModulePrefetchDelegate } from './providers/module-prefetch-delegate';
import { CoreCourseFormatDefaultHandler } from './providers/default-format';
import { CoreCourseFormatSingleActivityModule } from './formats/singleactivity/singleactivity.module';
import { CoreCourseFormatTopicsModule} from './formats/topics/topics.module';
import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module';
@NgModule({
declarations: [],
imports: [
CoreCourseFormatSingleActivityModule,
CoreCourseFormatTopicsModule,
CoreCourseFormatWeeksModule
],

View File

@ -0,0 +1,119 @@
// (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, OnChanges, ViewContainerRef, ComponentFactoryResolver, ChangeDetectorRef,
SimpleChange } from '@angular/core';
import { CoreLoggerProvider } from '../../../../../providers/logger';
import { CoreCourseModuleDelegate } from '../../../providers/module-delegate';
import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module';
/**
* Component to display single activity format. It will determine the right component to use and instantiate it.
*
* The instantiated component will receive the course and the module as inputs.
*/
@Component({
selector: 'core-course-format-single-activity',
template: ''
})
export class CoreCourseFormatSingleActivityComponent implements OnChanges {
@Input() course: any; // The course to render.
@Input() sections: any[]; // List of course sections.
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
protected logger: any;
protected module: any;
protected componentInstance: any;
constructor(logger: CoreLoggerProvider, private viewRef: ViewContainerRef, private factoryResolver: ComponentFactoryResolver,
private cdr: ChangeDetectorRef, private moduleDelegate: CoreCourseModuleDelegate) {
this.logger = logger.getInstance('CoreCourseFormatSingleActivityComponent');
}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}) {
if (this.course && this.sections && this.sections.length) {
// In single activity the module should only have 1 section and 1 module. Get the module.
let module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0];
if (module && !this.componentInstance) {
// We haven't created the component yet. Create it now.
this.createComponent(module);
}
if (this.componentInstance && this.componentInstance.ngOnChanges) {
// Call ngOnChanges of the component.
let newChanges: {[name: string]: SimpleChange} = {};
// Check if course has changed.
if (changes.course) {
newChanges.course = changes.course
this.componentInstance.course = this.course;
}
// Check if module has changed.
if (changes.sections && module != this.module) {
newChanges.module = {
currentValue: module,
firstChange: changes.sections.firstChange,
previousValue: this.module,
isFirstChange: () => {
return newChanges.module.firstChange;
}
};
this.componentInstance.module = module;
this.module = module;
}
if (Object.keys(newChanges).length) {
this.componentInstance.ngOnChanges(newChanges);
}
}
}
}
/**
* Create the component, add it to the container and set the input data.
*
* @param {any} module The module.
* @return {boolean} Whether the component was successfully created.
*/
protected createComponent(module: any) : boolean {
let componentClass = this.moduleDelegate.getMainComponent(this.course, module) || CoreCourseUnsupportedModuleComponent;
if (!componentClass) {
// No component to instantiate.
return false;
}
try {
// Create the component and add it to the container.
const factory = this.factoryResolver.resolveComponentFactory(componentClass),
componentRef = this.viewRef.createComponent(factory);
this.componentInstance = componentRef.instance;
// Set the Input data.
this.componentInstance.courseId = this.course.id;
this.componentInstance.module = module;
// this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed.
return true;
} catch(ex) {
this.logger.error('Error creating component', ex);
return false;
}
}
}

View File

@ -0,0 +1,83 @@
// (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 { CoreCourseFormatHandler } from '../../../providers/format-delegate';
import { CoreCourseFormatSingleActivityComponent } from '../components/format';
/**
* Handler to support weeks course format.
*/
@Injectable()
export class CoreCourseFormatSingleActivityHandler implements CoreCourseFormatHandler {
name = 'singleactivity';
constructor() {}
/**
* 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> {
return true;
}
/**
* 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 {
return false;
}
/**
* Get the title to use in course page. If not defined, course fullname.
* This function will be called without sections first, and then call it again when the sections are retrieved.
*
* @param {any} course The course.
* @param {any[]} [sections] List of sections.
* @return {string} Title.
*/
getCourseTitle(course: any, sections?: any[]) : string {
if (sections && sections[0] && sections[0].modules && sections[0].modules[0]) {
return sections[0].modules[0].name;
}
return course.fullname || '';
}
/**
* 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 {
return false;
}
/**
* 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 CoreCourseFormatSingleActivityComponent;
}
}

View File

@ -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 { CoreCourseFormatSingleActivityComponent } from './components/format';
import { CoreCourseFormatSingleActivityHandler } from './providers/handler';
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
@NgModule({
declarations: [
CoreCourseFormatSingleActivityComponent
],
imports: [
],
providers: [
CoreCourseFormatSingleActivityHandler
],
exports: [
CoreCourseFormatSingleActivityComponent
],
entryComponents: [
CoreCourseFormatSingleActivityComponent
]
})
export class CoreCourseFormatSingleActivityModule {
constructor(formatDelegate: CoreCourseFormatDelegate, handler: CoreCourseFormatSingleActivityHandler) {
formatDelegate.registerHandler(handler);
}
}

View File

@ -16,9 +16,10 @@
</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>
<!-- @todo: Use core-tabs or a new component. core-tabs might initialize all tabs at start, so we might require a new component. -->
<div class="core-tabs-bar">
<a aria-selected="true">{{ 'core.course.contents' | translate }}</a>
<a *ngFor="let handler of courseHandlers">{{ handler.data.title || translate }}</a>
</div>
<core-course-format [course]="course" [sections]="sections" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format>
</core-loading>

View File

@ -0,0 +1,24 @@
page-core-course-section {
.core-tabs-bar {
@include position(null, null, 0, 0);
z-index: $z-index-toolbar;
display: flex;
width: 100%;
background: $core-top-tabs-background;
> a {
@extend .tab-button;
background: $core-top-tabs-background;
color: $core-top-tabs-color !important;
border-bottom: 1px solid $core-top-tabs-border;
font-size: 1.6rem;
&[aria-selected=true] {
color: $core-top-tabs-color-active !important;
border-bottom: 2px solid $core-top-tabs-color-active;
}
}
}
}

View File

@ -58,9 +58,11 @@ export class CoreCourseSectionPage implements OnDestroy {
private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider,
sitesProvider: CoreSitesProvider) {
this.course = navParams.get('course');
this.title = courseFormatDelegate.getCourseTitle(this.course);
this.moduleId = navParams.get('moduleId');
// Get the title to display. We dont't have sections yet.
this.title = courseFormatDelegate.getCourseTitle(this.course);
this.completionObserver = eventsProvider.on(CoreEventsProvider.COMPLETION_MODULE_VIEWED, (data) => {
if (data && data.courseId == this.course.id) {
this.refreshAfterCompletionChange();
@ -150,6 +152,9 @@ export class CoreCourseSectionPage implements OnDestroy {
id: CoreCourseProvider.ALL_SECTIONS_ID
});
}
// Get the title again now that we have sections.
this.title = this.courseFormatDelegate.getCourseTitle(this.course, this.sections);
}));
}));

View File

@ -10,21 +10,6 @@
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding>
<core-course-module-description [description]="module.description"></core-course-module-description>
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
<h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2>
<p class="core-big" *ngIf="isDisabledInSite">{{ 'core.course.activitydisabled' | translate }}</p>
<p class="core-big" *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.course.activitynotyetviewablesiteupgradeneeded' | translate }}</p>
<p class="core-big" *ngIf="!isDisabledInSite && !isSupportedByTheApp">{{ 'core.course.activitynotyetviewableremoteaddon' | translate }}</p>
<p *ngIf="isDisabledInSite || !isSupportedByTheApp"><strong>{{ 'core.course.askadmintosupport' | translate }}</strong></p>
<div *ngIf="module.url">
<p><strong>{{ 'core.course.useactivityonbrowser' | translate }}</strong></p>
<a ion-button block icon-end [href]="module.url" core-link>
{{ 'core.openinbrowser' | translate }}
<ion-icon name="open"></ion-icon>
</a>
</div>
<ion-content>
<core-course-unsupported-module [module]="module"></core-course-unsupported-module>
</ion-content>

View File

@ -13,11 +13,9 @@
// limitations under the License.
import { Component } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { IonicPage, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
import { CoreCourseProvider } from '../../providers/course';
import { CoreCourseModuleDelegate } from '../../providers/module-delegate';
/**
* Page that displays info about an unsupported module.
@ -28,30 +26,18 @@ import { CoreCourseModuleDelegate } from '../../providers/module-delegate';
templateUrl: 'unsupported-module.html',
})
export class CoreCourseUnsupportedModulePage {
module: any;
isDisabledInSite: boolean;
isSupportedByTheApp: boolean;
moduleName: string;
constructor(navParams: NavParams, private translate: TranslateService, private textUtils: CoreTextUtilsProvider,
private moduleDelegate: CoreCourseModuleDelegate, private courseProvider: CoreCourseProvider) {
private navCtrl: NavController) {
this.module = navParams.get('module') || {};
}
/**
* View loaded.
*/
ionViewDidLoad() {
this.isDisabledInSite = this.moduleDelegate.isModuleDisabledInSite(this.module.modname);
this.isSupportedByTheApp = this.moduleDelegate.hasHandler(this.module.modname);
this.moduleName = this.courseProvider.translateModuleName(this.module.modname);
}
/**
* Expand the description.
*/
expandDescription() {
this.textUtils.expandText(this.translate.instant('core.description'), this.module.description, false);
this.textUtils.expandText(this.translate.instant('core.description'), this.module.description, false,
undefined, undefined, this.navCtrl);
}
}

View File

@ -39,11 +39,13 @@ export interface CoreCourseFormatHandler {
/**
* Get the title to use in course page. If not defined, course fullname.
* This function will be called without sections first, and then call it again when the sections are retrieved.
*
* @param {any} course The course.
* @param {any[]} [sections] List of sections.
* @return {string} Title.
*/
getCourseTitle?(course: any) : string;
getCourseTitle?(course: any, sections?: any[]) : string;
/**
* Whether it allows seeing all sections at the same time. Defaults to true.
@ -227,10 +229,11 @@ export class CoreCourseFormatDelegate {
* Given a course, return the title to use in the course page.
*
* @param {any} course The course to get the title.
* @param {any[]} [sections] List of sections.
* @return {string} Course title.
*/
getCourseTitle(course: any) : string {
return this.executeFunction(course.format, 'getCourseTitle', [course]);
getCourseTitle(course: any, sections?: any[]) : string {
return this.executeFunction(course.format, 'getCourseTitle', [course, sections]);
}
/**

View File

@ -52,6 +52,15 @@ export interface CoreCourseModuleHandler {
* @return {CoreCourseModuleHandlerData} Data to render the module.
*/
getData(module: any, courseId: number, sectionId: number) : CoreCourseModuleHandlerData;
/**
* Get the component to render the module. This is needed to support singleactivity course format.
*
* @param {any} course The course object.
* @param {any} module The module object.
* @return {any} The component to use, undefined if not found.
*/
getMainComponent(course: any, module: any) : any;
};
/**
@ -163,6 +172,23 @@ export class CoreCourseModuleDelegate {
eventsProvider.on(CoreEventsProvider.REMOTE_ADDONS_LOADED, this.updateHandlers.bind(this));
}
/**
* Get the component to render the module.
*
* @param {any} course The course object.
* @param {any} module The module object.
* @return {any} The component to use, undefined if not found.
*/
getMainComponent?(course: any, module: any) : any {
let handler = this.enabledHandlers[module.modname];
if (handler && handler.getMainComponent) {
let component = handler.getMainComponent(course, module);
if (component) {
return component;
}
}
}
/**
* Get the data required to display the module in the course contents view.
*

View File

@ -165,7 +165,7 @@ export class CoreFileUploaderDelegate {
protected lastUpdateHandlersStart: number;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) {
this.logger = logger.getInstance('CoreCourseModuleDelegate');
this.logger = logger.getInstance('CoreFileUploaderDelegate');
eventsProvider.on(CoreEventsProvider.LOGIN, this.updateHandlers.bind(this));
eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.updateHandlers.bind(this));

View File

@ -251,7 +251,7 @@ export class CoreTextUtilsProvider {
* @param {boolean} [isModal] Whether it should be opened in a modal (true) or in a new page (false).
* @param {string} [component] Component to link the embedded files to.
* @param {string|number} [componentId] An ID to use in conjunction with the component.
* @param {NavController} [navCtrl] The NavController instance to use.
* @param {NavController} [navCtrl] The NavController instance to use. Required if isModal is false.
*/
expandText(title: string, text: string, isModal?: boolean, component?: string, componentId?: string|number,
navCtrl?: NavController) : void {