diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 6eaaeb367..de5065205 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -20,7 +20,7 @@ - + diff --git a/src/components/components.module.ts b/src/components/components.module.ts index 7d62c20f4..d1dddb4fb 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -37,6 +37,8 @@ import { CoreSitePickerComponent } from './site-picker/site-picker'; import { CoreTabsComponent } from './tabs/tabs'; import { CoreTabComponent } from './tabs/tab'; import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor'; +import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons'; +import { CoreDynamicComponent } from './dynamic-component/dynamic-component'; @NgModule({ declarations: [ @@ -59,7 +61,9 @@ import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor CoreSitePickerComponent, CoreTabsComponent, CoreTabComponent, - CoreRichTextEditorComponent + CoreRichTextEditorComponent, + CoreNavBarButtonsComponent, + CoreDynamicComponent ], entryComponents: [ CoreContextMenuPopoverComponent, @@ -89,7 +93,9 @@ import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor CoreSitePickerComponent, CoreTabsComponent, CoreTabComponent, - CoreRichTextEditorComponent + CoreRichTextEditorComponent, + CoreNavBarButtonsComponent, + CoreDynamicComponent ] }) export class CoreComponentsModule {} diff --git a/src/components/dynamic-component/dynamic-component.html b/src/components/dynamic-component/dynamic-component.html new file mode 100644 index 000000000..99c89fec9 --- /dev/null +++ b/src/components/dynamic-component/dynamic-component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/dynamic-component/dynamic-component.ts b/src/components/dynamic-component/dynamic-component.ts new file mode 100644 index 000000000..01fa9532d --- /dev/null +++ b/src/components/dynamic-component/dynamic-component.ts @@ -0,0 +1,168 @@ +// (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, ViewChild, OnInit, OnChanges, DoCheck, ViewContainerRef, ComponentFactoryResolver, + KeyValueDiffers, SimpleChange +} from '@angular/core'; +import { CoreLoggerProvider } from '../../providers/logger'; + +/** + * Component to create another component dynamically. + * + * You need to pass the class of the component to this component (the class, not the name), along with the input data. + * + * So you should do something like: + * + * import { MyComponent } from './component'; + * + * ... + * + * this.component = MyComponent; + * + * And in the template: + * + * + * Cannot render the data. + * + * + * Please notice that the component that you pass needs to be declared in entryComponents of the module to be created dynamically. + * + * The contents of this component will be displayed if no component is supplied or it cannot be created. In the example above, + * if no component is supplied then the template will show the message "Cannot render the data.". + */ +@Component({ + selector: 'core-dynamic-component', + templateUrl: 'dynamic-component.html' +}) +export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck { + + @Input() component: any; + @Input() data: any; + + // Get the container where to put the dynamic component. + @ViewChild('dynamicComponent', { read: ViewContainerRef }) set dynamicComponent(el: ViewContainerRef) { + this.container = el; + this.createComponent(); + } + + instance: any; + container: ViewContainerRef; + protected logger: any; + protected differ: any; // To detect changes in the data input. + + constructor(logger: CoreLoggerProvider, private factoryResolver: ComponentFactoryResolver, differs: KeyValueDiffers) { + this.logger = logger.getInstance('CoreDynamicComponent'); + this.differ = differs.find([]).create(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.createComponent(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: { [name: string]: SimpleChange }): void { + if (!this.instance && changes.component) { + this.createComponent(); + } + } + + /** + * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays). + */ + ngDoCheck(): void { + if (this.instance) { + // Check if there's any change in the data object. + const changes = this.differ.diff(this.data); + if (changes) { + this.setInputData(); + if (this.instance.ngOnChanges) { + this.instance.ngOnChanges(this.createChangesForComponent(changes)); + } + } + } + } + + /** + * Create a component, add it to a container and set the input data. + * + * @return {boolean} Whether the component was successfully created. + */ + protected createComponent(): boolean { + if (!this.component || !this.container) { + // No component to instantiate or container doesn't exist right now. + return false; + } + + if (this.instance) { + // Component already instantiated. + return true; + } + + try { + // Create the component and add it to the container. + const factory = this.factoryResolver.resolveComponentFactory(this.component), + componentRef = this.container.createComponent(factory); + + this.instance = componentRef.instance; + + this.setInputData(); + + return true; + } catch (ex) { + this.logger.error('Error creating component', ex); + + return false; + } + } + + /** + * Set the input data for the component. + */ + protected setInputData(): void { + for (const name in this.data) { + this.instance[name] = this.data[name]; + } + } + + /** + * Given the changes on the data input, create the changes object for the component. + * + * @param {any} changes Changes in the data input (detected by KeyValueDiffer). + * @return {{[name: string]: SimpleChange}} List of changes for the component. + */ + protected createChangesForComponent(changes: any): { [name: string]: SimpleChange } { + const newChanges: { [name: string]: SimpleChange } = {}; + + // Added items are considered first change. + changes.forEachAddedItem((item) => { + newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true); + }); + + // Changed or removed items aren't first change. + changes.forEachChangedItem((item) => { + newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, false); + }); + changes.forEachRemovedItem((item) => { + newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true); + }); + + return newChanges; + } +} diff --git a/src/components/navbar-buttons/navbar-buttons.scss b/src/components/navbar-buttons/navbar-buttons.scss new file mode 100644 index 000000000..d800187c1 --- /dev/null +++ b/src/components/navbar-buttons/navbar-buttons.scss @@ -0,0 +1,3 @@ +core-navbar-buttons, .core-navbar-button-hidden { + display: none !important; +} diff --git a/src/components/navbar-buttons/navbar-buttons.ts b/src/components/navbar-buttons/navbar-buttons.ts new file mode 100644 index 000000000..98edb3421 --- /dev/null +++ b/src/components/navbar-buttons/navbar-buttons.ts @@ -0,0 +1,148 @@ +// (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, ContentChildren, ElementRef, QueryList } from '@angular/core'; +import { Button } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '../../providers/utils/dom'; + +/** + * Component to add buttons to the app's header without having to place them inside the header itself. This is meant for + * pages that are loaded inside a sub ion-nav, so they don't have a header. + * + * If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that + * position. If no start/end is specified, then the buttons will be added to the first found in the header. + * + * You can use the [hidden] input to hide all the inner buttons if a certain condition is met. + * + * Example usage: + * + * + * + * + * + * + */ +@Component({ + selector: 'core-navbar-buttons', + template: '' +}) +export class CoreNavBarButtonsComponent implements OnInit { + + protected BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden'; + + // If the hidden input is true, hide all buttons. + @Input('hidden') set hidden(value: boolean) { + this._hidden = value; + if (this._buttons) { + this._buttons.forEach((button: Button) => { + this.showHideButton(button); + }); + } + } + + // Get all the buttons inside this directive. + @ContentChildren(Button) set buttons(buttons: QueryList) { + this._buttons = buttons; + buttons.forEach((button: Button) => { + button.setRole('bar-button'); + this.showHideButton(button); + }); + } + + protected element: HTMLElement; + protected _buttons: QueryList; + protected _hidden: boolean; + + constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider) { + this.element = element.nativeElement; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + const header = this.searchHeader(); + + if (header) { + // Search the right buttons container (start, end or any). + let selector = 'ion-buttons', + buttonsContainer: HTMLElement; + + if (this.element.hasAttribute('start')) { + selector += '[start]'; + } else if (this.element.hasAttribute('end')) { + selector += '[end]'; + } + + buttonsContainer = header.querySelector(selector); + if (buttonsContainer) { + this.domUtils.moveChildren(this.element, buttonsContainer); + } + } + } + + /** + * Search the ion-header where the buttons should be added. + * + * @return {HTMLElement} Header element. + */ + protected searchHeader(): HTMLElement { + let parentPage: HTMLElement = this.element; + + while (parentPage) { + if (!parentPage.parentElement) { + // No parent, stop. + break; + } + + // Get the next parent page. + parentPage = this.domUtils.closest(parentPage.parentElement, '.ion-page'); + if (parentPage) { + // Check if the page has a header. If it doesn't, search the next parent page. + const header = this.searchHeaderInPage(parentPage); + if (header) { + return header; + } + } + } + } + + /** + * Search ion-header inside a page. The header should be a direct child. + * + * @param {HTMLElement} page Page to search in. + * @return {HTMLElement} Header element. Undefined if not found. + */ + protected searchHeaderInPage(page: HTMLElement): HTMLElement { + for (let i = 0; i < page.children.length; i++) { + const child = page.children[i]; + if (child.tagName == 'ION-HEADER') { + return child; + } + } + } + + /** + * Show or hide a button. + * + * @param {Button} button Button to show or hide. + */ + protected showHideButton(button: Button): void { + if (this._hidden) { + button.getNativeElement().classList.add(this.BUTTON_HIDDEN_CLASS); + } else { + button.getNativeElement().classList.remove(this.BUTTON_HIDDEN_CLASS); + } + } +} diff --git a/src/components/split-view/split-view.html b/src/components/split-view/split-view.html index dbea8c69b..6f8042dd3 100644 --- a/src/components/split-view/split-view.html +++ b/src/components/split-view/split-view.html @@ -1,6 +1,5 @@ - diff --git a/src/components/split-view/split-view.scss b/src/components/split-view/split-view.scss index 4383232eb..580884451 100644 --- a/src/components/split-view/split-view.scss +++ b/src/components/split-view/split-view.scss @@ -36,4 +36,16 @@ core-split-view { } } } +} + +.ios ion-header + core-split-view ion-menu.split-pane-side ion-content{ + top: $navbar-ios-height; +} + +.md ion-header + core-split-view ion-menu.split-pane-side ion-content{ + top: $navbar-md-height; +} + +.wp ion-header + core-split-view ion-menu.split-pane-side ion-content{ + top: $navbar-wp-height; } \ No newline at end of file diff --git a/src/components/split-view/split-view.ts b/src/components/split-view/split-view.ts index 51056847b..9ddf22670 100644 --- a/src/components/split-view/split-view.ts +++ b/src/components/split-view/split-view.ts @@ -47,6 +47,7 @@ export class CoreSplitViewComponent implements OnInit { @Input() when?: string | boolean = 'md'; protected isEnabled = false; protected masterPageName = ''; + protected masterPageIndex = 0; protected loadDetailPage: any = false; protected element: HTMLElement; // Current element. @@ -63,9 +64,32 @@ export class CoreSplitViewComponent implements OnInit { ngOnInit(): void { // Get the master page name and set an empty page as a placeholder. this.masterPageName = this.masterNav.getActive().component.name; + this.masterPageIndex = this.masterNav.indexOf(this.masterNav.getActive()); this.emptyDetails(); } + /** + * Get the details NavController. If split view is not enabled, it will return the master nav. + * + * @return {NavController} Details NavController. + */ + getDetailsNav(): NavController { + if (this.isEnabled) { + return this.detailNav; + } else { + return this.masterNav; + } + } + + /** + * Get the master NavController. + * + * @return {NavController} Master NavController. + */ + getMasterNav(): NavController { + return this.masterNav; + } + /** * Check if both panels are shown. It depends on screen width. * @@ -81,7 +105,7 @@ export class CoreSplitViewComponent implements OnInit { * @param {any} page The component class or deeplink name you want to push onto the navigation stack. * @param {any} params Any NavParams you want to pass along to the next view. */ - push(page: any, params?: any, element?: HTMLElement): void { + push(page: any, params?: any): void { if (this.isEnabled) { this.detailNav.setRoot(page, params); } else { @@ -119,17 +143,19 @@ export class CoreSplitViewComponent implements OnInit { activateSplitView(): void { const currentView = this.masterNav.getActive(), currentPageName = currentView.component.name; - if (currentPageName != this.masterPageName) { - // CurrentView is a 'Detail' page remove it from the 'master' nav stack. - this.masterNav.pop(); + if (this.masterNav.getPrevious().component.name == this.masterPageName) { + if (currentPageName != this.masterPageName) { + // CurrentView is a 'Detail' page remove it from the 'master' nav stack. + this.masterNav.pop(); - // And add it to the 'detail' nav stack. - this.detailNav.setRoot(currentView.component, currentView.data); - } else if (this.loadDetailPage) { - // MasterPage is shown, load the last detail page if found. - this.detailNav.setRoot(this.loadDetailPage.component, this.loadDetailPage.data); + // And add it to the 'detail' nav stack. + this.detailNav.setRoot(currentView.component, currentView.data); + } else if (this.loadDetailPage) { + // MasterPage is shown, load the last detail page if found. + this.detailNav.setRoot(this.loadDetailPage.component, this.loadDetailPage.data); + } + this.loadDetailPage = false; } - this.loadDetailPage = false; } /** @@ -140,7 +166,7 @@ export class CoreSplitViewComponent implements OnInit { currentPageName = detailView.component.name; if (currentPageName != 'CoreSplitViewPlaceholderPage') { // Current detail view is a 'Detail' page so, not the placeholder page, push it on 'master' nav stack. - this.masterNav.push(detailView.component, detailView.data); + this.masterNav.insert(this.masterPageIndex + 1, detailView.component, detailView.data); } } } diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index dd40b2d86..c68461533 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -36,10 +36,20 @@ core-tabs { core-tab { display: none; height: 100%; + position: relative; &.selected { display: block; } + + ion-header { + display: none; + } + + .fixed-content, .scroll-content { + margin-top: 0 !important; + margin-bottom: 0 !important; + } } } diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index f5e056984..9c9cee453 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -43,7 +43,7 @@ import { Content } from 'ionic-angular'; }) export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { @Input() selectedIndex = 0; // Index of the tab to select. - @Input() hideUntil: boolean; // Determine when should the contents be shown. + @Input() hideUntil = true; // Determine when should the contents be shown. @Output() ionChange: EventEmitter = new EventEmitter(); // Emitted when the tab changes. @ViewChild('originalTabs') originalTabsRef: ElementRef; @ViewChild('topTabs') topTabs: ElementRef; @@ -60,13 +60,8 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { protected tabsShown = true; protected scroll: HTMLElement; // Parent scroll element (if core-tabs is inside a ion-content). - constructor(element: ElementRef, content: Content) { + constructor(element: ElementRef, protected content: Content) { this.tabBarElement = element.nativeElement; - setTimeout(() => { - if (content) { - this.scroll = content.getScrollElement(); - } - }, 1); } /** @@ -148,7 +143,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { let selectedIndex = this.selectedIndex || 0, selectedTab = this.tabs[selectedIndex]; - if (!selectedTab.enabled || !selectedTab.show) { + if (!selectedTab || !selectedTab.enabled || !selectedTab.show) { // The tab is not enabled or not shown. Get the first tab that is enabled. selectedTab = this.tabs.find((tab, index) => { if (tab.enabled && tab.show) { @@ -168,8 +163,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { // Setup tab scrolling. this.tabBarHeight = this.topTabsElement.offsetHeight; this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px'; - if (this.scroll) { - this.scroll.classList.add('no-scroll'); + if (this.content) { + this.scroll = this.content.getScrollElement(); + if (this.scroll) { + this.scroll.classList.add('no-scroll'); + } } this.initialized = true; diff --git a/src/core/course/components/format/format.html b/src/core/course/components/format/format.html index a0fbe654f..0b1e8dcc8 100644 --- a/src/core/course/components/format/format.html +++ b/src/core/course/components/format/format.html @@ -1,45 +1,45 @@ - + - - = 0"> - - - - + + + = 0"> + + + + - - - - {{section.formattedName || section.name}} - - - - - + + + + + {{section.formattedName || section.name}} + + + + + - + - - + - + - - + - + @@ -78,6 +78,3 @@ 0 && section.count < section.total">{{section.count}} / {{section.total}} - - - \ No newline at end of file diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 626218146..578f9fe0f 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -12,13 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { - Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ComponentFactoryResolver, ViewChild, ChangeDetectorRef, - SimpleChange, Output, EventEmitter -} from '@angular/core'; +import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreLoggerProvider } from '../../../../providers/logger'; import { CoreSitesProvider } from '../../../../providers/sites'; import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; import { CoreCourseProvider } from '../../../course/providers/course'; @@ -48,31 +44,15 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { @Input() initialSectionNumber?: number; // The section to load first (by number). @Output() completionChanged?: EventEmitter; // Will emit an event when any module completion changes. - // 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); - } + // All the possible component classes. + courseFormatComponent: any; + courseSummaryComponent: any; + sectionSelectorComponent: any; + singleSectionComponent: any; + allSectionsComponent: any; - // Instances and containers of all the components that the handler could define. - protected componentContainers: { [type: string]: ViewContainerRef } = {}; - componentInstances: { [type: string]: any } = {}; + // Data to pass to the components. + data: any = {}; displaySectionSelector: boolean; selectedSection: any; @@ -80,23 +60,20 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { selectOptions: any = {}; loaded: boolean; - protected logger; protected sectionStatusObserver; - constructor(logger: CoreLoggerProvider, private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, - private factoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef, + constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, prefetchDelegate: CoreCourseModulePrefetchDelegate) { - this.logger = logger.getInstance('CoreCourseFormatComponent'); this.selectOptions.title = translate.instant('core.course.sections'); this.completionChanged = new EventEmitter(); // Listen for section status changes. this.sectionStatusObserver = eventsProvider.on(CoreEventsProvider.SECTION_STATUS_CHANGED, (data) => { if (this.downloadEnabled && this.sections && this.sections.length && this.course && data.sectionId && - data.courseId == this.course.id) { + data.courseId == this.course.id) { // Check if the affected section is being downloaded. // If so, we don't update section status because it'll already be updated when the download finishes. const downloadId = this.courseHelper.getSectionDownloadId({ id: data.sectionId }); @@ -135,15 +112,19 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { */ ngOnInit(): void { 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 }): void { + this.setInputData(); + + if (changes.course) { + // Course has changed, try to get the components. + this.getComponents(); + } + if (changes.sections && this.sections) { if (!this.selectedSection) { // There is no selected section yet, calculate which one to load. @@ -186,62 +167,39 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { if (changes.downloadEnabled && this.downloadEnabled) { this.calculateSectionsStatus(false); } - - // Apply the changes to the components and call ngOnChanges if it exists. - for (const type in this.componentInstances) { - const instance = this.componentInstances[type]; - - for (const 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. + * Set the input data for components. */ - 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; - } + protected setInputData(): void { + this.data.course = this.course; + this.data.sections = this.sections; + this.data.initialSectionId = this.initialSectionId; + this.data.initialSectionNumber = this.initialSectionNumber; + this.data.downloadEnabled = this.downloadEnabled; + } - 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; - - // Set the Input data. - this.componentInstances[type].course = this.course; - this.componentInstances[type].sections = this.sections; - this.componentInstances[type].initialSectionId = this.initialSectionId; - this.componentInstances[type].initialSectionNumber = this.initialSectionNumber; - 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); - - return false; + /** + * Get the components classes. + */ + protected getComponents(): void { + if (this.course) { + if (!this.courseFormatComponent) { + this.courseFormatComponent = this.cfDelegate.getCourseFormatComponent(this.course); + } + if (!this.courseSummaryComponent) { + this.courseSummaryComponent = this.cfDelegate.getCourseSummaryComponent(this.course); + } + if (!this.sectionSelectorComponent) { + this.sectionSelectorComponent = this.cfDelegate.getSectionSelectorComponent(this.course); + } + if (!this.singleSectionComponent) { + this.singleSectionComponent = this.cfDelegate.getSingleSectionComponent(this.course); + } + if (!this.allSectionsComponent) { + this.allSectionsComponent = this.cfDelegate.getAllSectionsComponent(this.course); + } } } @@ -253,16 +211,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { sectionChanged(newSection: any): void { const 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') - }); - } - } + this.data.section = this.selectedSection; } /** diff --git a/src/core/course/formats/singleactivity/components/format.ts b/src/core/course/formats/singleactivity/components/format.ts deleted file mode 100644 index 001e0e692..000000000 --- a/src/core/course/formats/singleactivity/components/format.ts +++ /dev/null @@ -1,117 +0,0 @@ -// (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, 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 moduleDelegate: CoreCourseModuleDelegate) { - this.logger = logger.getInstance('CoreCourseFormatSingleActivityComponent'); - } - - /** - * Detect changes on input properties. - */ - ngOnChanges(changes: { [name: string]: SimpleChange }): void { - if (this.course && this.sections && this.sections.length) { - // In single activity the module should only have 1 section and 1 module. Get the module. - const 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. - const 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: (): boolean => { - 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 { - const 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; - - return true; - } catch (ex) { - this.logger.error('Error creating component', ex); - - return false; - } - } -} diff --git a/src/core/course/formats/singleactivity/components/singleactivity.html b/src/core/course/formats/singleactivity/components/singleactivity.html new file mode 100644 index 000000000..1f3a37007 --- /dev/null +++ b/src/core/course/formats/singleactivity/components/singleactivity.html @@ -0,0 +1 @@ + diff --git a/src/core/course/formats/singleactivity/components/singleactivity.ts b/src/core/course/formats/singleactivity/components/singleactivity.ts new file mode 100644 index 000000000..cd5cae32c --- /dev/null +++ b/src/core/course/formats/singleactivity/components/singleactivity.ts @@ -0,0 +1,55 @@ +// (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, SimpleChange } from '@angular/core'; +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', + templateUrl: 'singleactivity.html' +}) +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. + + componentClass: any; // The class of the component to render. + data: any = {}; // Data to pass to the component. + + constructor(private moduleDelegate: CoreCourseModuleDelegate) { } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: { [name: string]: SimpleChange }): void { + if (this.course && this.sections && this.sections.length) { + // In single activity the module should only have 1 section and 1 module. Get the module. + const module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0]; + if (module && !this.componentClass) { + // We haven't obtained the class yet. Get it now. + this.componentClass = this.moduleDelegate.getMainComponent(this.course, module) || + CoreCourseUnsupportedModuleComponent; + } + + this.data.courseId = this.course.id; + this.data.module = module; + } + } +} diff --git a/src/core/course/formats/singleactivity/providers/handler.ts b/src/core/course/formats/singleactivity/providers/handler.ts index 1f232fe78..17f1a5734 100644 --- a/src/core/course/formats/singleactivity/providers/handler.ts +++ b/src/core/course/formats/singleactivity/providers/handler.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseFormatHandler } from '../../../providers/format-delegate'; -import { CoreCourseFormatSingleActivityComponent } from '../components/format'; +import { CoreCourseFormatSingleActivityComponent } from '../components/singleactivity'; /** * Handler to support singleactivity course format. diff --git a/src/core/course/formats/singleactivity/singleactivity.module.ts b/src/core/course/formats/singleactivity/singleactivity.module.ts index 0ccb98388..8899f01db 100644 --- a/src/core/course/formats/singleactivity/singleactivity.module.ts +++ b/src/core/course/formats/singleactivity/singleactivity.module.ts @@ -13,15 +13,17 @@ // limitations under the License. import { NgModule } from '@angular/core'; -import { CoreCourseFormatSingleActivityComponent } from './components/format'; +import { CoreCourseFormatSingleActivityComponent } from './components/singleactivity'; import { CoreCourseFormatSingleActivityHandler } from './providers/handler'; import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; +import { CoreComponentsModule } from '../../../../components/components.module'; @NgModule({ declarations: [ CoreCourseFormatSingleActivityComponent ], imports: [ + CoreComponentsModule ], providers: [ CoreCourseFormatSingleActivityHandler diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index f890b24f3..b57f35177 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -11,16 +11,26 @@ - - - + + + + + + + + - - - - {{ 'core.course.contents' | translate }} - {{ handler.data.title || translate }} - - - + + + + + + + + + + + + + diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index b33a10b48..06dd8666a 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -43,6 +43,7 @@ export class CoreCourseSectionPage implements OnDestroy { sectionId: number; sectionNumber: number; courseHandlers: CoreCourseOptionsHandlerToDisplay[]; + handlerData: any = {}; // Data to send to the handlers components. dataLoaded: boolean; downloadEnabled: boolean; downloadEnabledIcon = 'square-outline'; // Disabled by default. @@ -63,6 +64,7 @@ export class CoreCourseSectionPage implements OnDestroy { this.course = navParams.get('course'); this.sectionId = navParams.get('sectionId'); this.sectionNumber = navParams.get('sectionNumber'); + this.handlerData.courseId = this.course.id; // Get the title to display. We dont't have sections yet. this.title = courseFormatDelegate.getCourseTitle(this.course); @@ -122,11 +124,15 @@ export class CoreCourseSectionPage implements OnDestroy { */ protected loadData(refresh?: boolean): Promise { // First of all, get the course because the data might have changed. - return this.coursesProvider.getUserCourse(this.course.id).then((course) => { + return this.coursesProvider.getUserCourse(this.course.id).catch(() => { + // Error getting the course, probably guest access. + }).then((course) => { const promises = []; let promise; - this.course = course; + if (course) { + this.course = course; + } // Get the completion status. if (this.course.enablecompletion === false) { diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index ec74e848f..6cb0e43aa 100644 --- a/src/core/course/providers/options-delegate.ts +++ b/src/core/course/providers/options-delegate.ts @@ -33,7 +33,6 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler { /** * Whether or not the handler is enabled for a certain course. - * For perfomance reasons, do NOT call WebServices in here, call them in shouldDisplayForCourse. * * @param {number} courseId The course ID. * @param {any} accessData Access type and data. Default, guest, ... @@ -43,17 +42,6 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler { */ isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise; - /** - * Whether or not the handler should be displayed for a course. If not implemented, assume it's true. - * - * @param {number} courseId The course ID. - * @param {any} accessData Access type and data. Default, guest, ... - * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. - * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. - * @return {boolean|Promise} True or promise resolved with true if enabled. - */ - shouldDisplayForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise; - /** * Returns the data needed to render the handler. * @@ -91,12 +79,6 @@ export interface CoreCourseOptionsHandlerData { */ title: string; - /** - * Name of the icon to display for the handler. - * @type {string} - */ - icon: string; - /** * Class to add to the displayed handler. * @type {string} @@ -104,11 +86,10 @@ export interface CoreCourseOptionsHandlerData { class?: string; /** - * Action to perform when the handler is clicked. - * - * @param {any} course The course. + * The component to render the handler. It must be the component class, not the name or an instance. + * When the component is created, it will receive the courseId as input. */ - action(course: any): void; + component: any; } /** @@ -282,38 +263,22 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { // Call getHandlersForAccess to make sure the handlers have been loaded. return this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions); }).then(() => { - const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = [], - promises = []; - let promise; + const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = []; this.coursesHandlers[course.id].enabledHandlers.forEach((handler) => { - if (handler.shouldDisplayForCourse) { - promise = Promise.resolve(handler.shouldDisplayForCourse( - course.id, accessData, course.navOptions, course.admOptions)); - } else { - // Not implemented, assume it should be displayed. - promise = Promise.resolve(true); - } - - promises.push(promise.then((enabled) => { - if (enabled) { - handlersToDisplay.push({ - data: handler.getDisplayData(course), - priority: handler.priority, - prefetch: handler.prefetch - }); - } - })); - }); - - return this.utils.allPromises(promises).then(() => { - // Sort them by priority. - handlersToDisplay.sort((a, b) => { - return b.priority - a.priority; + handlersToDisplay.push({ + data: handler.getDisplayData(course), + priority: handler.priority, + prefetch: handler.prefetch }); - - return handlersToDisplay; }); + + // Sort them by priority. + handlersToDisplay.sort((a, b) => { + return b.priority - a.priority; + }); + + return handlersToDisplay; }); } @@ -451,8 +416,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { * @param {any} accessData Access type and data. Default, guest, ... * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. - * @return {Promise} Resolved when updated. - * @protected + * @return {Promise} Resolved when updated. */ updateHandlersForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): Promise { const promises = [], diff --git a/src/core/courses/pages/course-preview/course-preview.html b/src/core/courses/pages/course-preview/course-preview.html index ec071512d..c01bec3db 100644 --- a/src/core/courses/pages/course-preview/course-preview.html +++ b/src/core/courses/pages/course-preview/course-preview.html @@ -10,7 +10,7 @@ - + {{course.categoryname}} @@ -41,24 +41,15 @@ {{ 'core.courses.notenrollable' | translate }} - + {{ 'core.course.downloadcourse' | translate }} - + {{ 'core.course.contents' | translate }} - - - - - - - - - diff --git a/src/core/courses/pages/course-preview/course-preview.ts b/src/core/courses/pages/course-preview/course-preview.ts index 966148945..81a97cb18 100644 --- a/src/core/courses/pages/course-preview/course-preview.ts +++ b/src/core/courses/pages/course-preview/course-preview.ts @@ -36,8 +36,7 @@ import { CoreCourseHelperProvider } from '../../../course/providers/helper'; export class CoreCoursesCoursePreviewPage implements OnDestroy { course: any; isEnrolled: boolean; - handlersShouldBeShown = true; - handlersLoaded: boolean; + canAccessCourse = true; component = 'CoreCoursesCoursePreview'; selfEnrolInstances: any[] = []; paypalEnabled: boolean; @@ -206,19 +205,17 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { // 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; - - return this.loadCourseHandlers(refresh, false); + this.canAccessCourse = true; }).catch(() => { // The user is not an admin/manager. Check if we can provide guest access to the course. return this.canAccessAsGuest().then((passwordRequired) => { if (!passwordRequired) { - return this.loadCourseHandlers(refresh, true); + this.canAccessCourse = true; } else { - return Promise.reject(null); + this.canAccessCourse = false; } }).catch(() => { - this.course._handlers = []; - this.handlersShouldBeShown = false; + this.canAccessCourse = false; }); }); }).finally(() => { @@ -226,25 +223,11 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { }); } - /** - * Load course nav handlers. - * - * @param {boolean} refresh Whether the user is refreshing the data. - * @param {boolean} guest Whether it's guest access. - */ - protected loadCourseHandlers(refresh: boolean, guest: boolean): Promise { - return this.courseOptionsDelegate.getHandlersToDisplay(this.course, refresh, guest, true).then((handlers) => { - this.course._handlers = handlers; - this.handlersShouldBeShown = true; - this.handlersLoaded = true; - }); - } - /** * Open the course. */ openCourse(): void { - if (!this.handlersShouldBeShown) { + if (!this.canAccessCourse) { // Course cannot be opened. return; } @@ -435,8 +418,7 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { * Prefetch the course. */ prefetchCourse(): void { - this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course, undefined, this.course._handlers) - .catch((error) => { + this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course).catch((error) => { if (!this.pageDestroyed) { this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); } diff --git a/src/core/user/components/components.module.ts b/src/core/user/components/components.module.ts index 13682bbe7..215319610 100644 --- a/src/core/user/components/components.module.ts +++ b/src/core/user/components/components.module.ts @@ -16,21 +16,33 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; +import { CoreUserParticipantsComponent } from './participants/participants'; import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field'; +import { CoreComponentsModule } from '../../../components/components.module'; +import { CoreDirectivesModule } from '../../../directives/directives.module'; +import { CorePipesModule } from '../../../pipes/pipes.module'; @NgModule({ declarations: [ + CoreUserParticipantsComponent, CoreUserProfileFieldComponent ], imports: [ CommonModule, IonicModule, TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule ], providers: [ ], exports: [ + CoreUserParticipantsComponent, CoreUserProfileFieldComponent + ], + entryComponents: [ + CoreUserParticipantsComponent ] }) export class CoreUserComponentsModule {} diff --git a/src/core/user/components/participants/participants.html b/src/core/user/components/participants/participants.html new file mode 100644 index 000000000..f089dd40f --- /dev/null +++ b/src/core/user/components/participants/participants.html @@ -0,0 +1,25 @@ + + + + + + + + + + 0" no-margin> + + + + + + {{ 'core.lastaccess' | translate }}: {{ participant.lastaccess * 1000 | coreFormatDate:"dfmediumdate"}} + + + + + + + + + \ No newline at end of file diff --git a/src/core/user/components/participants/participants.ts b/src/core/user/components/participants/participants.ts new file mode 100644 index 000000000..675be0144 --- /dev/null +++ b/src/core/user/components/participants/participants.ts @@ -0,0 +1,103 @@ +// (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, ViewChild, Input, OnInit } from '@angular/core'; +import { Content, NavParams } from 'ionic-angular'; +import { CoreUserProvider } from '../../providers/user'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreSplitViewComponent } from '../../../../components/split-view/split-view'; + +/** + * Component that displays the list of course participants. + */ +@Component({ + selector: 'core-user-participants', + templateUrl: 'participants.html', +}) +export class CoreUserParticipantsComponent implements OnInit { + @ViewChild(Content) content: Content; + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + @Input() courseId: number; + + participantId: number; + participants = []; + canLoadMore = false; + participantsLoaded = false; + + constructor(private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider) { } + + /** + * View loaded. + */ + ngOnInit(): void { + // Get first participants. + this.fetchData(true).then(() => { + if (!this.participantId && this.splitviewCtrl.isOn() && this.participants.length > 0) { + // Take first and load it. + this.gotoParticipant(this.participants[0].id); + } + // Add log in Moodle. + this.userProvider.logParticipantsView(this.courseId).catch(() => { + // Ignore errors. + }); + }).finally(() => { + this.participantsLoaded = true; + }); + } + + /** + * Fetch all the data required for the view. + * + * @param {boolean} [refresh] Empty events array first. + * @return {Promise} Resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + const firstToGet = refresh ? 0 : this.participants.length; + + return this.userProvider.getParticipants(this.courseId, firstToGet).then((data) => { + if (refresh) { + this.participants = data.participants; + } else { + this.participants = this.participants.concat(data.participants); + } + this.canLoadMore = data.canLoadMore; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error loading participants'); + this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading. + }); + } + + /** + * Refresh data. + * + * @param {any} refresher Refresher. + */ + refreshParticipants(refresher: any): void { + this.userProvider.invalidateParticipantsList(this.courseId).finally(() => { + this.fetchData(true).finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Navigate to a particular user profile. + * @param {number} userId User Id where to navigate. + */ + gotoParticipant(userId: number): void { + this.participantId = userId; + this.splitviewCtrl.push('CoreUserProfilePage', {userId: userId, courseId: this.courseId}); + } +} diff --git a/src/core/user/components/user-profile-field/user-profile-field.html b/src/core/user/components/user-profile-field/user-profile-field.html index 825590342..1f3a37007 100644 --- a/src/core/user/components/user-profile-field/user-profile-field.html +++ b/src/core/user/components/user-profile-field/user-profile-field.html @@ -1,2 +1 @@ - - \ No newline at end of file + diff --git a/src/core/user/components/user-profile-field/user-profile-field.ts b/src/core/user/components/user-profile-field/user-profile-field.ts index 0058e9a9a..7cbd32d4f 100644 --- a/src/core/user/components/user-profile-field/user-profile-field.ts +++ b/src/core/user/components/user-profile-field/user-profile-field.ts @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, OnInit } from '@angular/core'; -import { CoreLoggerProvider } from '../../../../providers/logger'; +import { Component, Input, OnInit } from '@angular/core'; import { CoreUserProfileFieldDelegate } from '../../providers/user-profile-field-delegate'; import { CoreUtilsProvider } from '../../../../providers/utils/utils'; @@ -31,74 +30,23 @@ export class CoreUserProfileFieldComponent implements OnInit { @Input() form?: any; // Form where to add the form control. Required if edit=true or signup=true. @Input() registerAuth?: string; // Register auth method. E.g. 'email'. - // Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf. - @ViewChild('userProfileField', { read: ViewContainerRef }) set userProfileField(el: ViewContainerRef) { - if (this.field) { - this.createComponent(this.ufDelegate.getComponent(this.field, this.signup), el); - } else { - // The component hasn't been initialized yet. Store the container. - this.fieldContainer = el; - } - } + componentClass: any; // The class of the component to render. + data: any = {}; // Data to pass to the component. - protected logger; - - // Instances and containers of all the components that the handler could define. - protected fieldContainer: ViewContainerRef; - protected fieldInstance: any; - - constructor(logger: CoreLoggerProvider, private factoryResolver: ComponentFactoryResolver, - private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider) { - this.logger = logger.getInstance('CoreUserProfileFieldComponent'); - } + constructor(private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider) { } /** * Component being initialized. */ ngOnInit(): void { - this.createComponent(this.ufDelegate.getComponent(this.field, this.signup), this.fieldContainer); - } + this.componentClass = this.ufDelegate.getComponent(this.field, this.signup); - /** - * Create a component, add it to a container and set the input data. - * - * @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(componentClass: any, container: ViewContainerRef): boolean { - if (!componentClass || !container) { - // No component to instantiate or container doesn't exist right now. - return false; - } - - if (this.fieldInstance && container === this.fieldContainer) { - // 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.fieldContainer = container; - this.fieldInstance = componentRef.instance; - - // Set the Input data. - this.fieldInstance.field = this.field; - this.fieldInstance.edit = this.utilsProvider.isTrueOrOne(this.edit); - if (this.edit) { - this.fieldInstance.signup = this.utilsProvider.isTrueOrOne(this.signup); - this.fieldInstance.disabled = this.utilsProvider.isTrueOrOne(this.field.locked); - this.fieldInstance.form = this.form; - } - - return true; - } catch (ex) { - this.logger.error('Error creating user field component', ex, componentClass); - - return false; + this.data.field = this.field; + this.data.edit = this.utilsProvider.isTrueOrOne(this.edit); + if (this.edit) { + this.data.signup = this.utilsProvider.isTrueOrOne(this.signup); + this.data.disabled = this.utilsProvider.isTrueOrOne(this.field.locked); + this.data.form = this.form; } } } diff --git a/src/core/user/lang/en.json b/src/core/user/lang/en.json index d5390b8ac..0c5fb6889 100644 --- a/src/core/user/lang/en.json +++ b/src/core/user/lang/en.json @@ -15,6 +15,8 @@ "lastname": "Surname", "manager": "Manager", "newpicture": "New picture", + "noparticipants": "No participants found for this course.", + "participants": "Participants", "phone1": "Phone", "phone2": "Mobile phone", "roles": "Roles", diff --git a/src/core/user/pages/participants/participants.html b/src/core/user/pages/participants/participants.html new file mode 100644 index 000000000..ec0f41e69 --- /dev/null +++ b/src/core/user/pages/participants/participants.html @@ -0,0 +1,6 @@ + + + {{ 'core.user.participants' | translate }} + + + \ No newline at end of file diff --git a/src/core/user/pages/participants/participants.module.ts b/src/core/user/pages/participants/participants.module.ts new file mode 100644 index 000000000..29089cd65 --- /dev/null +++ b/src/core/user/pages/participants/participants.module.ts @@ -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 { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreUserComponentsModule } from '../../components/components.module'; +import { CoreUserParticipantsPage } from './participants'; + +@NgModule({ + declarations: [ + CoreUserParticipantsPage, + ], + imports: [ + CoreUserComponentsModule, + IonicPageModule.forChild(CoreUserParticipantsPage), + TranslateModule.forChild() + ], +}) +export class CoreUserParticipantsPageModule {} diff --git a/src/core/user/pages/participants/participants.ts b/src/core/user/pages/participants/participants.ts new file mode 100644 index 000000000..ae2a470c8 --- /dev/null +++ b/src/core/user/pages/participants/participants.ts @@ -0,0 +1,32 @@ +// (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'; + +/** + * Page that displays the list of course participants. + */ +@IonicPage({segment: 'core-user-participants'}) +@Component({ + selector: 'page-core-user-participants', + templateUrl: 'participants.html', +}) +export class CoreUserParticipantsPage { + courseId: number; + + constructor(navParams: NavParams) { + this.courseId = navParams.get('courseId'); + } +} diff --git a/src/core/user/pages/profile/profile.html b/src/core/user/pages/profile/profile.html index fda94e955..ae17d5f1d 100644 --- a/src/core/user/pages/profile/profile.html +++ b/src/core/user/pages/profile/profile.html @@ -26,7 +26,7 @@ - + {{comHandler.title | translate}} @@ -37,7 +37,7 @@ - + {{ 'core.user.details' | translate }} @@ -45,13 +45,13 @@ - + {{ npHandler.title | translate }} - + {{ actHandler.title | translate }} diff --git a/src/core/user/pages/profile/profile.ts b/src/core/user/pages/profile/profile.ts index 63b88ba5a..0c188c07c 100644 --- a/src/core/user/pages/profile/profile.ts +++ b/src/core/user/pages/profile/profile.ts @@ -12,8 +12,8 @@ // 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 { Component, Optional } from '@angular/core'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { CoreUserProvider } from '../../providers/user'; import { CoreUserHelperProvider } from '../../providers/helper'; import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; @@ -23,7 +23,8 @@ import { CoreEventsProvider } from '../../../../providers/events'; import { CoreSitesProvider } from '../../../../providers/sites'; import { CoreMimetypeUtilsProvider } from '../../../../providers/utils/mimetype'; import { CoreFileUploaderHelperProvider } from '../../../fileuploader/providers/helper'; -import { CoreUserDelegate } from '../../providers/user-delegate'; +import { CoreUserDelegate, CoreUserProfileHandlerData } from '../../providers/user-delegate'; +import { CoreSplitViewComponent } from '../../../../components/split-view/split-view'; /** * Page that displays an user profile page. @@ -45,15 +46,16 @@ export class CoreUserProfilePage { title: string; isDeleted = false; canChangeProfilePicture = false; - actionHandlers = []; - newPageHandlers = []; - communicationHandlers = []; + actionHandlers: CoreUserProfileHandlerData[] = []; + newPageHandlers: CoreUserProfileHandlerData[] = []; + communicationHandlers: CoreUserProfileHandlerData[] = []; constructor(navParams: NavParams, private userProvider: CoreUserProvider, private userHelper: CoreUserHelperProvider, private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private eventsProvider: CoreEventsProvider, private coursesProvider: CoreCoursesProvider, private sitesProvider: CoreSitesProvider, private mimetypeUtils: CoreMimetypeUtilsProvider, private fileUploaderHelper: CoreFileUploaderHelperProvider, - private userDelegate: CoreUserDelegate) { + private userDelegate: CoreUserDelegate, private navCtrl: NavController, + @Optional() private svComponent: CoreSplitViewComponent) { this.userId = navParams.get('userId'); this.courseId = navParams.get('courseId'); @@ -181,6 +183,27 @@ export class CoreUserProfilePage { }); } + /** + * Open the page with the user details. + */ + openUserDetails(): void { + // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + navCtrl.push('CoreUserAboutPage', {courseId: this.courseId, userId: this.userId}); + } + + /** + * A handler was clicked. + * + * @param {Event} event Click event. + * @param {CoreUserProfileHandlerData} handler Handler that was clicked. + */ + handlerClicked(event: Event, handler: CoreUserProfileHandlerData): void { + // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + handler.action(event, navCtrl, this.user, this.courseId); + } + /** * Page destroyed. */ diff --git a/src/core/user/providers/course-option-handler.ts b/src/core/user/providers/course-option-handler.ts new file mode 100644 index 000000000..633b5fde2 --- /dev/null +++ b/src/core/user/providers/course-option-handler.ts @@ -0,0 +1,92 @@ +// (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 { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate'; +import { CoreCourseProvider } from '../../course/providers/course'; +import { CoreUserProvider } from './user'; +import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreUserParticipantsComponent } from '../components/participants/participants'; + +/** + * Course nav handler. + */ +@Injectable() +export class CoreUserParticipantsCourseOptionHandler implements CoreCourseOptionsHandler { + name = 'AddonParticipants'; + priority = 600; + + constructor(private userProvider: CoreUserProvider, private loginHelper: CoreLoginHelperProvider) {} + + /** + * Should invalidate the data to determine if the handler is enabled for a certain course. + * + * @param {number} courseId The course ID. + * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return {Promise} Promise resolved when done. + */ + invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise { + if (navOptions && typeof navOptions.participants != 'undefined') { + // No need to invalidate anything. + return Promise.resolve(); + } + + return this.userProvider.invalidateParticipantsList(courseId); + } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether or not the handler is enabled for a certain course. + * + * @param {number} courseId The course ID. + * @param {any} accessData Access type and data. Default, guest, ... + * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { + if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { + return false; // Not enabled for guests. + } + + if (navOptions && typeof navOptions.participants != 'undefined') { + return navOptions.participants; + } + + return this.userProvider.isPluginEnabledForCourse(courseId); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreMainMenuHandlerData} Data needed to render the handler. + */ + getDisplayData(): CoreCourseOptionsHandlerData { + return { + title: 'core.user.participants', + class: 'core-user-participants-handler', + component: CoreUserParticipantsComponent + }; + } +} diff --git a/src/core/user/providers/participants-link-handler.ts b/src/core/user/providers/participants-link-handler.ts new file mode 100644 index 000000000..d2d46c695 --- /dev/null +++ b/src/core/user/providers/participants-link-handler.ts @@ -0,0 +1,74 @@ +// (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 { CoreContentLinksHandlerBase } from '../../../core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../../core/contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '../../../core/login/providers/helper'; +import { CoreUserProvider } from './user'; + +/** + * Handler to treat links to user participants page. + */ +@Injectable() +export class CoreUserParticipantsLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonParticipants'; + featureName = '$mmCoursesDelegate_mmaParticipants'; + pattern = /\/user\/index\.php/; + + constructor(private userProvider: CoreUserProvider, private loginHelper: CoreLoginHelperProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + courseId = parseInt(params.id, 10) || courseId; + + return [{ + action: (siteId, navCtrl?): void => { + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.loginHelper.redirect('AddonParticipantsListPage', {courseId: courseId}, siteId); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + courseId = parseInt(params.id, 10) || courseId; + + if (!courseId || url.indexOf('/grade/report/') != -1) { + return false; + } + + return this.userProvider.isPluginEnabledForCourse(courseId, siteId); + } +} diff --git a/src/core/user/providers/user-delegate.ts b/src/core/user/providers/user-delegate.ts index 0787ff32b..62452c596 100644 --- a/src/core/user/providers/user-delegate.ts +++ b/src/core/user/providers/user-delegate.ts @@ -13,12 +13,16 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; import { CoreCoursesProvider } from '../../../core/courses/providers/courses'; import { CoreLoggerProvider } from '../../../providers/logger'; import { CoreSitesProvider } from '../../../providers/sites'; import { CoreEventsProvider } from '../../../providers/events'; +/** + * Interface that all user profile handlers must implement. + */ export interface CoreUserProfileHandler extends CoreDelegateHandler { /** * The highest priority is displayed first. @@ -55,6 +59,9 @@ export interface CoreUserProfileHandler extends CoreDelegateHandler { getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData; } +/** + * Data needed to render a user profile handler. It's returned by the handler. + */ export interface CoreUserProfileHandlerData { /** * Title to display. @@ -88,12 +95,36 @@ export interface CoreUserProfileHandlerData { /** * Action to do when clicked. - * @param {any} $event - * @param {any} user User object. - * @param {number} courseId Course ID where to show. - * @return {any} Action to be done. + * + * @param {Event} event Click event. + * @param {NavController} Nav controller to use to navigate. + * @param {any} user User object. + * @param {number} [courseId] Course ID being viewed. If not defined, site context. */ - action?($event: any, user: any, courseId: number): any; + action?(event: Event, navCtrl: NavController, user: any, courseId?: number): void; +} + +/** + * Data returned by the delegate for each handler. + */ +export interface CoreUserProfileHandlerToDisplay { + /** + * Data to display. + * @type {CoreUserProfileHandlerData} + */ + data: CoreUserProfileHandlerData; + + /** + * The highest priority is displayed first. + * @type {number} + */ + priority?: number; + + /** + * The type of the handler. See CoreUserProfileHandler. + * @type {string} + */ + type: string; } /** @@ -133,10 +164,10 @@ export class CoreUserDelegate extends CoreDelegate { * * @param {any} user The user object. * @param {number} courseId The course ID. - * @return {Promise} Resolved with an array of objects containing 'priority', 'data' and 'type'. + * @return {Promise} Resolved with the handlers. */ - getProfileHandlersFor(user: any, courseId: number): Promise { - const handlers = [], + getProfileHandlersFor(user: any, courseId: number): Promise { + const handlers: CoreUserProfileHandlerToDisplay[] = [], promises = []; // Retrieve course options forcing cache. diff --git a/src/core/user/providers/user-handler.ts b/src/core/user/providers/user-handler.ts index 0c50827a1..e8127290f 100644 --- a/src/core/user/providers/user-handler.ts +++ b/src/core/user/providers/user-handler.ts @@ -60,9 +60,9 @@ export class CoreUserProfileMailHandler implements CoreUserProfileHandler { icon: 'mail', title: 'core.user.sendemail', class: 'core-user-profile-mail', - action: ($event, user, courseId): void => { - $event.preventDefault(); - $event.stopPropagation(); + action: (event, navCtrl, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); window.open('mailto:' + user.email, '_blank'); } }; diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts index b2502f788..b6af8f055 100644 --- a/src/core/user/providers/user.ts +++ b/src/core/user/providers/user.ts @@ -23,6 +23,7 @@ import { CoreUtilsProvider } from '../../../providers/utils/utils'; */ @Injectable() export class CoreUserProvider { + static PARTICIPANTS_LIST_LIMIT = 50; // Max of participants to retrieve in each WS call. static PROFILE_REFRESHED = 'CoreUserProfileRefreshed'; static PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated'; protected ROOT_CACHE_KEY = 'mmUser:'; @@ -106,6 +107,59 @@ export class CoreUserProvider { return Promise.all(promises); } + /** + * Get participants for a certain course. + * + * @param {number} courseId ID of the course. + * @param {number} limitFrom Position of the first participant to get. + * @param {number} limitNumber Number of participants to get. + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise to be resolved when the participants are retrieved. + */ + getParticipants(courseId: number, limitFrom: number = 0, limitNumber: number = CoreUserProvider.PARTICIPANTS_LIST_LIMIT, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + this.logger.debug(`Get participants for course '${courseId}' starting at '${limitFrom}'`); + + const data = { + courseid: courseId, + options: [ + { + name: 'limitfrom', + value: limitFrom + }, + { + name: 'limitnumber', + value: limitNumber + }, + { + name: 'sortby', + value: 'siteorder' + } + ] + }, preSets = { + cacheKey: this.getParticipantsListCacheKey(courseId) + }; + + return site.read('core_enrol_get_enrolled_users', data, preSets).then((users) => { + const canLoadMore = users.length >= limitNumber; + this.storeUsers(users, siteId); + + return { participants: users, canLoadMore: canLoadMore }; + }); + }); + } + + /** + * Get cache key for participant list WS calls. + * + * @param {number} courseId Course ID. + * @return {string} Cache key. + */ + protected getParticipantsListCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'list:' + courseId; + } + /** * Get user profile. The type of profile retrieved depends on the params. * @@ -218,6 +272,59 @@ export class CoreUserProvider { }); } + /** + * Invalidates participant list for a certain course. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the list is invalidated. + */ + invalidateParticipantsList(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getParticipantsListCacheKey(courseId)); + }); + } + + /** + * Check if course participants is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isParticipantsDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isParticipantsDisabledInSite(site); + }); + } + + /** + * Check if course participants is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isParticipantsDisabledInSite(site?: any): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('$mmCoursesDelegate_mmaParticipants'); + } + + /** + * Returns whether or not the participants addon is enabled for a certain course. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + */ + isPluginEnabledForCourse(courseId: number, siteId?: string): Promise { + if (!courseId) { + return Promise.reject(null); + } + + // Retrieving one participant will fail if browsing users is disabled by capabilities. + return this.utils.promiseWorks(this.getParticipants(courseId, 0, 1, siteId)); + } + /** * Check if update profile picture is disabled in a certain site. * @@ -248,6 +355,17 @@ export class CoreUserProvider { return this.sitesProvider.getCurrentSite().write('core_user_view_user_profile', params); } + /** + * Log Participants list view in Moodle. + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved when done. + */ + logParticipantsView(courseId?: number): Promise { + return this.sitesProvider.getCurrentSite().write('core_user_view_user_list', { + courseid: courseId + }); + } + /** * Store user basic information in local DB to be retrieved if the WS call fails. * @@ -268,4 +386,23 @@ export class CoreUserProvider { return site.getDb().insertOrUpdateRecord(this.USERS_TABLE, userRecord, { id: userId }); }); } + + /** + * Store users basic information in local DB. + * + * @param {any[]} users Users to store. Fields stored: id, fullname, profileimageurl. + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolve when the user is stored. + */ + storeUsers(users: any[], siteId?: string): Promise { + const promises = []; + + users.forEach((user) => { + if (typeof user.id != 'undefined') { + promises.push(this.storeUser(user.id, user.fullname, user.profileimageurl, siteId)); + } + }); + + return Promise.all(promises); + } } diff --git a/src/core/user/user.module.ts b/src/core/user/user.module.ts index 4f86bb899..92588ca57 100644 --- a/src/core/user/user.module.ts +++ b/src/core/user/user.module.ts @@ -22,11 +22,16 @@ import { CoreEventsProvider } from '../../providers/events'; import { CoreSitesProvider } from '../../providers/sites'; import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; import { CoreUserProfileLinkHandler } from './providers/user-link-handler'; +import { CoreUserParticipantsCourseOptionHandler } from './providers/course-option-handler'; +import { CoreUserParticipantsLinkHandler } from './providers/participants-link-handler'; +import { CoreCourseOptionsDelegate } from '../course/providers/options-delegate'; +import { CoreUserComponentsModule } from './components/components.module'; @NgModule({ declarations: [ ], imports: [ + CoreUserComponentsModule ], providers: [ CoreUserDelegate, @@ -34,15 +39,22 @@ import { CoreUserProfileLinkHandler } from './providers/user-link-handler'; CoreUserProfileMailHandler, CoreUserProvider, CoreUserHelperProvider, - CoreUserProfileLinkHandler + CoreUserProfileLinkHandler, + CoreUserParticipantsCourseOptionHandler, + CoreUserParticipantsLinkHandler ] }) export class CoreUserModule { constructor(userDelegate: CoreUserDelegate, userProfileMailHandler: CoreUserProfileMailHandler, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, userProvider: CoreUserProvider, - contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler) { + contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler, + courseOptionHandler: CoreUserParticipantsCourseOptionHandler, linkHandler: CoreUserParticipantsLinkHandler, + courseOptionsDelegate: CoreCourseOptionsDelegate) { + userDelegate.registerHandler(userProfileMailHandler); + courseOptionsDelegate.registerHandler(courseOptionHandler); contentLinksDelegate.registerHandler(userLinkHandler); + contentLinksDelegate.registerHandler(linkHandler); eventsProvider.on(CoreEventsProvider.USER_DELETED, (data) => { // Search for userid in params. diff --git a/src/lang/en.json b/src/lang/en.json index 32ec7dfd3..6ca3ca4cf 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -96,6 +96,7 @@ "info": "Info", "ios": "iOS", "labelsep": ": ", + "lastaccess": "Last access", "lastdownloaded": "Last downloaded", "lastmodified": "Last modified", "lastsync": "Last synchronization",
Cannot render the data.
{{course.categoryname}}
{{ 'core.courses.notenrollable' | translate }}
{{ 'core.lastaccess' | translate }}: {{ participant.lastaccess * 1000 | coreFormatDate:"dfmediumdate"}}
{{comHandler.title | translate}}