MOBILE-2338 data: Implement index page
parent
a4b1dd0c73
commit
bbc6fcdff5
|
@ -0,0 +1,28 @@
|
||||||
|
// (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 { Input } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for component to render a field.
|
||||||
|
*/
|
||||||
|
export class AddonModDataFieldPluginComponent {
|
||||||
|
@Input() mode: string; // The render mode.
|
||||||
|
@Input() field: any; // The field to render.
|
||||||
|
@Input() value?: any; // The value of the field.
|
||||||
|
@Input() database?: any; // Database object.
|
||||||
|
@Input() error?: string; // Error when editing.
|
||||||
|
@Input() viewAction: string; // Action to perform.
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||||
|
import { AddonModDataIndexComponent } from './index/index';
|
||||||
|
import { AddonModDataFieldPluginComponent } from './field-plugin/field-plugin';
|
||||||
|
import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModDataIndexComponent,
|
||||||
|
AddonModDataFieldPluginComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreCourseComponentsModule,
|
||||||
|
CoreCompileHtmlComponentModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModDataIndexComponent,
|
||||||
|
AddonModDataFieldPluginComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonModDataIndexComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModDataComponentsModule {}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<core-dynamic-component [component]="fieldComponent" [data]="data">
|
||||||
|
<!-- This content will be replaced by the component if found. -->
|
||||||
|
<core-loading [hideUntil]="fieldLoaded">
|
||||||
|
</core-loading>
|
||||||
|
</core-dynamic-component>
|
|
@ -0,0 +1,82 @@
|
||||||
|
// (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, Injector, ViewChild } from '@angular/core';
|
||||||
|
import { AddonModDataProvider } from '../../providers/data';
|
||||||
|
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
|
||||||
|
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays an assignment feedback plugin.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-data-field-plugin',
|
||||||
|
templateUrl: 'field-plugin.html',
|
||||||
|
})
|
||||||
|
export class AddonModDataFieldPluginComponent implements OnInit {
|
||||||
|
@ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent;
|
||||||
|
|
||||||
|
@Input() mode: string; // The render mode.
|
||||||
|
@Input() field: any; // The field to render.
|
||||||
|
@Input() value?: any; // The value of the field.
|
||||||
|
@Input() database?: any; // Database object.
|
||||||
|
@Input() error?: string; // Error when editing.
|
||||||
|
@Input() viewAction: string; // Action to perform.
|
||||||
|
|
||||||
|
fieldComponent: any; // Component to render the plugin.
|
||||||
|
data: any; // Data to pass to the component.
|
||||||
|
fieldLoaded: boolean;
|
||||||
|
|
||||||
|
constructor(protected injector: Injector, protected dataDelegate: AddonModDataFieldsDelegate,
|
||||||
|
protected dataProvider: AddonModDataProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
console.error('HERE');
|
||||||
|
if (!this.field) {
|
||||||
|
this.fieldLoaded = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the plugin has defined its own component to render itself.
|
||||||
|
this.dataDelegate.getComponentForField(this.injector, this.field).then((component) => {
|
||||||
|
this.fieldComponent = component;
|
||||||
|
|
||||||
|
if (component) {
|
||||||
|
// Prepare the data to pass to the component.
|
||||||
|
this.data = {
|
||||||
|
mode: this.mode,
|
||||||
|
field: this.field,
|
||||||
|
value: this.value,
|
||||||
|
database: this.database,
|
||||||
|
error: this.error,
|
||||||
|
viewAction: this.viewAction
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.fieldLoaded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the plugin data.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
invalidate(): Promise<any> {
|
||||||
|
return Promise.resolve(this.dynamicComponent && this.dynamicComponent.callComponentFunction('invalidate', []));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
<!-- Buttons to add to the header. -->
|
||||||
|
<core-navbar-buttons end>
|
||||||
|
<button *ngIf="canSearch" ion-button icon-only (click)="showSearch($event)" [attr.aria-label]="'addon.mod_data.search' | translate">
|
||||||
|
<ion-icon name="search"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item [priority]="500" *ngIf="canAdd" [content]="'addon.mod_data.addentries' | translate" [iconAction]="'add'" (action)="gotoAddEntries($event)"></core-context-menu-item>
|
||||||
|
<core-context-menu-item [priority]="400" *ngIf="firstEntry" [content]="'addon.mod_data.single' | translate" [iconAction]="'document'" (action)="gotoEntry(firstEntry)"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="300" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="size" [priority]="200" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
</core-navbar-buttons>
|
||||||
|
|
||||||
|
<!-- Content. -->
|
||||||
|
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||||
|
|
||||||
|
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
|
||||||
|
|
||||||
|
<!-- Data done in offline but not synchronized -->
|
||||||
|
<div class="core-warning-card" icon-start *ngIf="hasOffline">
|
||||||
|
<ion-icon name="warning"></ion-icon>
|
||||||
|
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-item text-wrap *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
|
||||||
|
<ion-label id="addon-data-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
|
||||||
|
<ion-label id="addon-data-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
|
||||||
|
<ion-select [(ngModel)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-data-groupslabel">
|
||||||
|
<ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<div class="core-info-card" icon-start *ngIf="!access.timeavailable && timeAvailableFrom">
|
||||||
|
<ion-icon name="information-circle"></ion-icon>
|
||||||
|
{{ 'addon.mod_data.notopenyet' | translate:{$a: timeAvailableFromReadable} }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="core-info-card" icon-start *ngIf="!access.timeavailable && timeAvailableTo">
|
||||||
|
<ion-icon name="information-circle"></ion-icon>
|
||||||
|
{{ 'addon.mod_data.expired' | translate:{$a: timeAvailableToReadable} }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="core-info-card" icon-start *ngIf="access.entrieslefttoview">
|
||||||
|
<ion-icon name="information-circle"></ion-icon>
|
||||||
|
{{ 'addon.mod_data.entrieslefttoaddtoview' | translate:{$a: {entrieslefttoview: access.entrieslefttoview} } }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="core-info-card" icon-start *ngIf="access.entrieslefttoadd">
|
||||||
|
<ion-icon name="information-circle"></ion-icon>
|
||||||
|
{{ 'addon.mod_data.entrieslefttoadd' | translate:{$a: {entriesleft: access.entrieslefttoadd} } }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-item class="item" *ngIf="search.searching && !isEmpty">
|
||||||
|
<a (click)="searchReset()">{{ 'addon.mod_data.resetsettings' | translate}}</a>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<div class="core-data-contents addon-data-entries-{{data.id}}" *ngIf="!isEmpty">
|
||||||
|
<style *ngIf="cssTemplate">
|
||||||
|
{{ cssTemplate }}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<core-compile-html [text]="entriesRendered"></core-compile-html>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-grid *ngIf="search.page > 0 || hasNextPage">
|
||||||
|
<ion-row align-items-center>
|
||||||
|
<ion-col *ngIf="search.page > 0">
|
||||||
|
<button ion-button block outline icon-start (click)="searchEntries(search.page - 1)">
|
||||||
|
<ion-icon name="arrow-back"></ion-icon>
|
||||||
|
{{ 'core.previous' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngIf="hasNextPage">
|
||||||
|
<button ion-button block icon-end (click)="searchEntries(search.page + 1)">
|
||||||
|
{{ 'core.next' | translate }}
|
||||||
|
<ion-icon name="arrow-forward"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
|
||||||
|
<core-empty-box *ngIf="isEmpty && !search.searching" icon="archive" [message]="'addon.mod_data.norecords' | translate">
|
||||||
|
<div padding-top *ngIf="canAdd">
|
||||||
|
<button block (click)="gotoAddEntries($event)">
|
||||||
|
{{ 'addon.mod_data.addentries' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</core-empty-box>
|
||||||
|
|
||||||
|
<core-empty-box *ngIf="isEmpty && search.searching" icon="archive" [message]="'addon.mod_data.nomatch' | translate">
|
||||||
|
<a (click)="searchReset()">{{ 'addon.mod_data.resetsettings' | translate}}</a>
|
||||||
|
</core-empty-box>
|
||||||
|
|
||||||
|
|
||||||
|
</core-loading>
|
|
@ -0,0 +1,3 @@
|
||||||
|
addon-mod-data-index {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,454 @@
|
||||||
|
// (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, Optional, Injector } from '@angular/core';
|
||||||
|
import { Content, ModalController } from 'ionic-angular';
|
||||||
|
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
|
||||||
|
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
|
||||||
|
import { CoreCommentsProvider } from '@core/comments/providers/comments';
|
||||||
|
import { AddonModDataProvider } from '../../providers/data';
|
||||||
|
import { AddonModDataHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModDataOfflineProvider } from '../../providers/offline';
|
||||||
|
import { AddonModDataSyncProvider } from '../../providers/sync';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a data index page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-data-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComponent {
|
||||||
|
component = AddonModDataProvider.COMPONENT;
|
||||||
|
moduleName = 'data';
|
||||||
|
|
||||||
|
access: any = {};
|
||||||
|
data: any = {};
|
||||||
|
fields: any;
|
||||||
|
selectedGroup: number;
|
||||||
|
advancedSearch: any;
|
||||||
|
timeAvailableFrom: number | boolean;
|
||||||
|
timeAvailableFromReadable: string | boolean;
|
||||||
|
timeAvailableTo: number | boolean;
|
||||||
|
timeAvailableToReadable: string | boolean;
|
||||||
|
isEmpty = false;
|
||||||
|
groupInfo: CoreGroupInfo;
|
||||||
|
entries = {};
|
||||||
|
firstEntry = false;
|
||||||
|
canAdd = false;
|
||||||
|
canSearch = false;
|
||||||
|
search = {
|
||||||
|
sortBy: '0',
|
||||||
|
sortDirection: 'DESC',
|
||||||
|
page: 0,
|
||||||
|
text: '',
|
||||||
|
searching: false,
|
||||||
|
searchingAdvanced: false,
|
||||||
|
advanced: []
|
||||||
|
};
|
||||||
|
hasNextPage = false;
|
||||||
|
offlineActions: any;
|
||||||
|
offlineEntries: any;
|
||||||
|
entriesRendered = '';
|
||||||
|
cssTemplate = '';
|
||||||
|
|
||||||
|
protected syncEventName = AddonModDataSyncProvider.AUTO_SYNCED;
|
||||||
|
protected entryChangedObserver: any;
|
||||||
|
protected hasComments = false;
|
||||||
|
|
||||||
|
constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider,
|
||||||
|
private dataOffline: AddonModDataOfflineProvider, @Optional() private content: Content,
|
||||||
|
private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider,
|
||||||
|
private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider,
|
||||||
|
private modalCtrl: ModalController, private utils: CoreUtilsProvider) {
|
||||||
|
super(injector);
|
||||||
|
|
||||||
|
// Refresh entries on change.
|
||||||
|
this.entryChangedObserver = this.eventsProvider.on(AddonModDataProvider.ENTRY_CHANGED, (eventData) => {
|
||||||
|
if (this.data.id == eventData.dataId) {
|
||||||
|
this.loaded = false;
|
||||||
|
|
||||||
|
return this.loadContent(true);
|
||||||
|
}
|
||||||
|
}, this.siteId);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
this.selectedGroup = this.group || 0;
|
||||||
|
|
||||||
|
this.loadContent(false, true).then(() => {
|
||||||
|
this.dataProvider.logView(this.data.id).then(() => {
|
||||||
|
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup search modal.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the invalidate content function.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected invalidateContent(): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(this.dataProvider.invalidateDatabaseData(this.courseId));
|
||||||
|
if (this.data) {
|
||||||
|
promises.push(this.dataProvider.invalidateDatabaseAccessInformationData(this.data.id));
|
||||||
|
promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule));
|
||||||
|
promises.push(this.dataProvider.invalidateEntriesData(this.data.id));
|
||||||
|
if (this.hasComments) {
|
||||||
|
promises.push(this.commentsProvider.invalidateCommentsByInstance('module', this.data.coursemodule));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares sync event data with current data to check if refresh content is needed.
|
||||||
|
*
|
||||||
|
* @param {any} syncEventData Data receiven on sync observer.
|
||||||
|
* @return {boolean} True if refresh is needed, false otherwise.
|
||||||
|
*/
|
||||||
|
protected isRefreshSyncNeeded(syncEventData: any): boolean {
|
||||||
|
if (this.data && syncEventData.dataId == this.data.id && typeof syncEventData.entryId == 'undefined') {
|
||||||
|
this.loaded = false;
|
||||||
|
// Refresh the data.
|
||||||
|
this.content.scrollToTop();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download data contents.
|
||||||
|
*
|
||||||
|
* @param {boolean} [refresh=false] If it's refreshing content.
|
||||||
|
* @param {boolean} [sync=false] If the refresh is needs syncing.
|
||||||
|
* @param {boolean} [showErrors=false] If show errors to the user of hide them.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||||
|
let canAdd = false,
|
||||||
|
canSearch = false;
|
||||||
|
|
||||||
|
return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => {
|
||||||
|
this.data = data;
|
||||||
|
|
||||||
|
this.description = data.intro || data.description;
|
||||||
|
this.dataRetrieved.emit(data);
|
||||||
|
|
||||||
|
if (sync) {
|
||||||
|
// Try to synchronize the data.
|
||||||
|
return this.syncActivity(showErrors).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
return this.dataProvider.getDatabaseAccessInformation(this.data.id);
|
||||||
|
}).then((accessData) => {
|
||||||
|
this.access = accessData;
|
||||||
|
|
||||||
|
if (!accessData.timeavailable) {
|
||||||
|
const time = this.timeUtils.timestamp();
|
||||||
|
|
||||||
|
this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ?
|
||||||
|
parseInt(this.data.timeavailablefrom, 10) * 1000 : false;
|
||||||
|
this.timeAvailableFromReadable = this.timeAvailableFrom ?
|
||||||
|
moment(this.timeAvailableFrom).format('LLL') : false;
|
||||||
|
this.timeAvailableTo = this.data.timeavailableto && time > this.data.timeavailableto ?
|
||||||
|
parseInt(this.data.timeavailableto, 10) * 1000 : false;
|
||||||
|
this.timeAvailableToReadable = this.timeAvailableTo ? moment(this.timeAvailableTo).format('LLL') : false;
|
||||||
|
|
||||||
|
this.isEmpty = true;
|
||||||
|
this.groupInfo = null;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canSearch = true;
|
||||||
|
canAdd = accessData.canaddentry;
|
||||||
|
|
||||||
|
return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule, accessData.canmanageentries)
|
||||||
|
.then((groupInfo) => {
|
||||||
|
this.groupInfo = groupInfo;
|
||||||
|
|
||||||
|
// Check selected group is accessible.
|
||||||
|
if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) {
|
||||||
|
if (!groupInfo.groups.some((group) => this.selectedGroup == group.id)) {
|
||||||
|
this.selectedGroup = groupInfo.groups[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fetchOfflineEntries();
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
return this.dataProvider.getFields(this.data.id).then((fields) => {
|
||||||
|
if (fields.length == 0) {
|
||||||
|
canSearch = false;
|
||||||
|
canAdd = false;
|
||||||
|
}
|
||||||
|
this.search.advanced = [];
|
||||||
|
|
||||||
|
this.fields = {};
|
||||||
|
fields.forEach((field) => {
|
||||||
|
this.fields[field.id] = field;
|
||||||
|
});
|
||||||
|
this.fields = this.utils.objectToArray(this.fields);
|
||||||
|
this.advancedSearch = this.dataHelper.displayAdvancedSearchFields(this.data.asearchtemplate, this.fields);
|
||||||
|
|
||||||
|
return this.fetchEntriesData();
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
// All data obtained, now fill the context menu.
|
||||||
|
this.fillContextMenu(refresh);
|
||||||
|
}).finally(() => {
|
||||||
|
this.canAdd = canAdd;
|
||||||
|
this.canSearch = canSearch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch current database entries.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Resolved then done.
|
||||||
|
*/
|
||||||
|
protected fetchEntriesData(): Promise<any> {
|
||||||
|
this.hasComments = false;
|
||||||
|
|
||||||
|
return this.dataProvider.getDatabaseAccessInformation(this.data.id, this.selectedGroup).then((accessData) => {
|
||||||
|
// Update values for current group.
|
||||||
|
this.access.canaddentry = accessData.canaddentry;
|
||||||
|
|
||||||
|
if (this.search.searching) {
|
||||||
|
const text = this.search.searchingAdvanced ? undefined : this.search.text,
|
||||||
|
advanced = this.search.searchingAdvanced ? this.search.advanced : undefined;
|
||||||
|
|
||||||
|
return this.dataProvider.searchEntries(this.data.id, this.selectedGroup, text, advanced, this.search.sortBy,
|
||||||
|
this.search.sortDirection, this.search.page);
|
||||||
|
} else {
|
||||||
|
return this.dataProvider.getEntries(this.data.id, this.selectedGroup, this.search.sortBy, this.search.sortDirection,
|
||||||
|
this.search.page);
|
||||||
|
}
|
||||||
|
}).then((entries) => {
|
||||||
|
const numEntries = (entries && entries.entries && entries.entries.length) || 0;
|
||||||
|
this.isEmpty = !numEntries && !Object.keys(this.offlineActions).length && !Object.keys(this.offlineEntries).length;
|
||||||
|
this.hasNextPage = numEntries >= AddonModDataProvider.PER_PAGE && ((this.search.page + 1) *
|
||||||
|
AddonModDataProvider.PER_PAGE) < entries.totalcount;
|
||||||
|
this.entriesRendered = '';
|
||||||
|
|
||||||
|
if (!this.isEmpty) {
|
||||||
|
this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.addon-data-entries-' + this.data.id);
|
||||||
|
|
||||||
|
const siteInfo = this.sitesProvider.getCurrentSite().getInfo(),
|
||||||
|
promises = [];
|
||||||
|
|
||||||
|
this.utils.objectToArray(this.offlineEntries).forEach((offlineActions) => {
|
||||||
|
const offlineEntry = offlineActions.find((offlineEntry) => offlineEntry.action == 'add');
|
||||||
|
|
||||||
|
if (offlineEntry) {
|
||||||
|
const entry = {
|
||||||
|
id: offlineEntry.entryid,
|
||||||
|
canmanageentry: true,
|
||||||
|
approved: !this.data.approval || this.data.manageapproved,
|
||||||
|
dataid: offlineEntry.dataid,
|
||||||
|
groupid: offlineEntry.groupid,
|
||||||
|
timecreated: -offlineEntry.entryid,
|
||||||
|
timemodified: -offlineEntry.entryid,
|
||||||
|
userid: siteInfo.userid,
|
||||||
|
fullname: siteInfo.fullname,
|
||||||
|
contents: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (offlineActions.length > 0) {
|
||||||
|
promises.push(this.dataHelper.applyOfflineActions(entry, offlineActions, this.fields));
|
||||||
|
} else {
|
||||||
|
promises.push(Promise.resolve(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
entries.entries.forEach((entry) => {
|
||||||
|
// Index contents by fieldid.
|
||||||
|
const contents = {};
|
||||||
|
entry.contents.forEach((field) => {
|
||||||
|
contents[field.fieldid] = field;
|
||||||
|
});
|
||||||
|
entry.contents = contents;
|
||||||
|
|
||||||
|
if (typeof this.offlineActions[entry.id] != 'undefined') {
|
||||||
|
promises.push(this.dataHelper.applyOfflineActions(entry, this.offlineActions[entry.id], this.fields));
|
||||||
|
} else {
|
||||||
|
promises.push(Promise.resolve(entry));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then((entries) => {
|
||||||
|
let entriesHTML = this.data.listtemplateheader || '';
|
||||||
|
|
||||||
|
// Get first entry from the whole list.
|
||||||
|
if (entries && entries[0] && (!this.search.searching || !this.firstEntry)) {
|
||||||
|
this.firstEntry = entries[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
this.entries[entry.id] = entry;
|
||||||
|
|
||||||
|
const actions = this.dataHelper.getActions(this.data, this.access, entry);
|
||||||
|
|
||||||
|
entriesHTML += this.dataHelper.displayShowFields(this.data.listtemplate, this.fields, entry, 'list',
|
||||||
|
actions);
|
||||||
|
});
|
||||||
|
entriesHTML += this.data.listtemplatefooter || '';
|
||||||
|
|
||||||
|
this.entriesRendered = entriesHTML;
|
||||||
|
});
|
||||||
|
} else if (!this.search.searching) {
|
||||||
|
// Empty and no searching.
|
||||||
|
this.canSearch = false;
|
||||||
|
}
|
||||||
|
this.firstEntry = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the chat users modal.
|
||||||
|
*/
|
||||||
|
showSearch(): void {
|
||||||
|
const modal = this.modalCtrl.create('AddonModDataSearchPage');
|
||||||
|
modal.onDidDismiss((data) => {
|
||||||
|
// @TODO.
|
||||||
|
});
|
||||||
|
modal.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the search and closes the modal.
|
||||||
|
*
|
||||||
|
* @param {number} page Page number.
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
searchEntries(page: number): Promise<any> {
|
||||||
|
this.loaded = false;
|
||||||
|
this.search.page = page;
|
||||||
|
|
||||||
|
if (this.search.searchingAdvanced) {
|
||||||
|
this.search.advanced = this.dataHelper.getSearchDataFromForm(document.forms['addon-mod_data-advanced-search-form'],
|
||||||
|
this.fields);
|
||||||
|
this.search.searching = this.search.advanced.length > 0;
|
||||||
|
} else {
|
||||||
|
this.search.searching = this.search.text.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fetchEntriesData().catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all search filters and closes the modal.
|
||||||
|
*/
|
||||||
|
searchReset(): void {
|
||||||
|
this.search.sortBy = '0';
|
||||||
|
this.search.sortDirection = 'DESC';
|
||||||
|
this.search.text = '';
|
||||||
|
this.search.advanced = [];
|
||||||
|
this.search.searchingAdvanced = false;
|
||||||
|
this.search.searching = false;
|
||||||
|
this.searchEntries(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set group to see the database.
|
||||||
|
setGroup(groupId: number): Promise<any> {
|
||||||
|
this.selectedGroup = groupId;
|
||||||
|
|
||||||
|
return this.fetchEntriesData().catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch offline entries.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Resolved then done.
|
||||||
|
*/
|
||||||
|
protected fetchOfflineEntries(): Promise<any> {
|
||||||
|
// Check if there are entries stored in offline.
|
||||||
|
return this.dataOffline.getDatabaseEntries(this.data.id).then((offlineEntries) => {
|
||||||
|
this.hasOffline = !!offlineEntries.length;
|
||||||
|
|
||||||
|
this.offlineActions = {};
|
||||||
|
this.offlineEntries = {};
|
||||||
|
|
||||||
|
// Only show offline entries on first page.
|
||||||
|
if (this.search.page == 0 && this.hasOffline) {
|
||||||
|
offlineEntries.forEach((entry) => {
|
||||||
|
if (entry.entryid > 0) {
|
||||||
|
if (typeof this.offlineActions[entry.entryid] == 'undefined') {
|
||||||
|
this.offlineActions[entry.entryid] = [];
|
||||||
|
}
|
||||||
|
this.offlineActions[entry.entryid].push(entry);
|
||||||
|
} else {
|
||||||
|
if (typeof this.offlineActions[entry.entryid] == 'undefined') {
|
||||||
|
this.offlineEntries[entry.entryid] = [];
|
||||||
|
}
|
||||||
|
this.offlineEntries[entry.entryid].push(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the sync of the activity.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected sync(): Promise<any> {
|
||||||
|
return this.dataSync.syncDatabase(this.data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if sync has succeed from result sync data.
|
||||||
|
*
|
||||||
|
* @param {any} result Data returned on the sync function.
|
||||||
|
* @return {boolean} If suceed or not.
|
||||||
|
*/
|
||||||
|
protected hasSyncSucceed(result: any): boolean {
|
||||||
|
return result.updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.entryChangedObserver && this.entryChangedObserver.off();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
// (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 { CoreCronDelegate } from '@providers/cron';
|
||||||
|
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||||
|
import { AddonModDataComponentsModule } from './components/components.module';
|
||||||
|
import { AddonModDataModuleHandler } from './providers/module-handler';
|
||||||
|
import { AddonModDataProvider } from './providers/data';
|
||||||
|
import { AddonModDataLinkHandler } from './providers/link-handler';
|
||||||
|
import { AddonModDataHelperProvider } from './providers/helper';
|
||||||
|
import { AddonModDataPrefetchHandler } from './providers/prefetch-handler';
|
||||||
|
import { AddonModDataSyncProvider } from './providers/sync';
|
||||||
|
import { AddonModDataSyncCronHandler } from './providers/sync-cron-handler';
|
||||||
|
import { AddonModDataOfflineProvider } from './providers/offline';
|
||||||
|
import { AddonModDataFieldsDelegate } from './providers/fields-delegate';
|
||||||
|
import { AddonModDataDefaultFieldHandler } from './providers/default-field-handler';
|
||||||
|
import { AddonModDataFieldModule } from './fields/field.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AddonModDataComponentsModule,
|
||||||
|
AddonModDataFieldModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonModDataProvider,
|
||||||
|
AddonModDataModuleHandler,
|
||||||
|
AddonModDataPrefetchHandler,
|
||||||
|
AddonModDataHelperProvider,
|
||||||
|
AddonModDataLinkHandler,
|
||||||
|
AddonModDataSyncCronHandler,
|
||||||
|
AddonModDataSyncProvider,
|
||||||
|
AddonModDataOfflineProvider,
|
||||||
|
AddonModDataFieldsDelegate,
|
||||||
|
AddonModDataDefaultFieldHandler
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModDataModule {
|
||||||
|
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModDataModuleHandler,
|
||||||
|
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModDataPrefetchHandler,
|
||||||
|
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModDataLinkHandler,
|
||||||
|
cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler) {
|
||||||
|
moduleDelegate.registerHandler(moduleHandler);
|
||||||
|
prefetchDelegate.registerHandler(prefetchHandler);
|
||||||
|
contentLinksDelegate.registerHandler(linkHandler);
|
||||||
|
cronDelegate.register(syncHandler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { AddonModDataFieldCheckboxHandler } from './providers/handler';
|
||||||
|
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
|
||||||
|
import { AddonModDataFieldCheckboxComponent } from './component/checkbox';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModDataFieldCheckboxComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonModDataFieldCheckboxHandler
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModDataFieldCheckboxComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonModDataFieldCheckboxComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModDataFieldCheckboxModule {
|
||||||
|
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldCheckboxHandler) {
|
||||||
|
fieldDelegate.registerHandler(handler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<ion-list *ngIf="mode != 'show'">
|
||||||
|
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required"></span>
|
||||||
|
<ion-item *ngFor="let option of options" [formGroup]="form">
|
||||||
|
<ion-label>{{ option }}</ion-label>
|
||||||
|
<ion-checkbox item-end [formControlName]="'f_'+field.id" [(ngModel)]="values[option]">
|
||||||
|
</ion-checkbox>
|
||||||
|
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorMessages]="errors"></core-input-errors>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item *ngIf="mode == 'search'" [formGroup]="form">
|
||||||
|
<ion-label>{{ 'addon.mod_data.selectedrequired' | translate }}</ion-label>
|
||||||
|
<ion-checkbox item-end [formControlName]="'f_'+field.id+'_allreq'" [(ngModel)]="values['f_'+field.id+'_allreq']">
|
||||||
|
</ion-checkbox>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
<core-format-text *ngIf="mode == 'show' && value && value.content" [text]="value.content"></core-format-text>
|
|
@ -0,0 +1,66 @@
|
||||||
|
// (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, OnInit, ElementRef } from '@angular/core';
|
||||||
|
import { FormBuilder, FormControl } from '@angular/forms';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render data checkbox field.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-data-field-checkbox',
|
||||||
|
templateUrl: 'checkbox.html'
|
||||||
|
})
|
||||||
|
export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginComponent implements OnInit {
|
||||||
|
|
||||||
|
control: FormControl;
|
||||||
|
options: number;
|
||||||
|
values = {};
|
||||||
|
|
||||||
|
constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider,
|
||||||
|
element: ElementRef) {
|
||||||
|
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.mode = this.mode == 'list' ? 'show' : this.mode;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): void {
|
||||||
|
if (this.mode == 'show') {
|
||||||
|
this.value.content.split('##').join('<br>');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options = this.field.param1.split('\n');
|
||||||
|
|
||||||
|
if (this.mode == 'edit' && this.value) {
|
||||||
|
this.values = {};
|
||||||
|
|
||||||
|
this.value.content.split('##').forEach((value) => {
|
||||||
|
this.values[value] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
//this.control = this.fb.control(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
// (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 { Injector, Injectable } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { AddonModDataFieldHandler } from '../../../providers/fields-delegate';
|
||||||
|
import { AddonModDataFieldCheckboxComponent } from '../component/checkbox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for checkbox data field plugin.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataFieldCheckboxHandler implements AddonModDataFieldHandler {
|
||||||
|
name = 'AddonModDataFieldCheckboxHandler';
|
||||||
|
type = 'checkbox';
|
||||||
|
|
||||||
|
constructor(private translate: TranslateService) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Component to use to display the plugin data.
|
||||||
|
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||||
|
*
|
||||||
|
* @param {Injector} injector Injector.
|
||||||
|
* @param {any} field The field object.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(injector: Injector, plugin: any): any | Promise<any> {
|
||||||
|
console.error(AddonModDataFieldCheckboxComponent);
|
||||||
|
return AddonModDataFieldCheckboxComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field search data in the input data.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the search form.
|
||||||
|
* @return {any} With name and value of the data to be sent.
|
||||||
|
*/
|
||||||
|
getFieldSearchData(field: any, inputData: any): any {
|
||||||
|
const fieldName = 'f_' + field.id,
|
||||||
|
reqName = 'f_' + field.id + '_allreq';
|
||||||
|
|
||||||
|
const checkboxes = [],
|
||||||
|
values = [];
|
||||||
|
inputData[fieldName].forEach((value, option) => {
|
||||||
|
if (value) {
|
||||||
|
checkboxes.push(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (checkboxes.length > 0) {
|
||||||
|
values.push({
|
||||||
|
name: fieldName,
|
||||||
|
value: checkboxes
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inputData[reqName]['1']) {
|
||||||
|
values.push({
|
||||||
|
name: reqName,
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field edit data in the input data.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @return {any} With name and value of the data to be sent.
|
||||||
|
*/
|
||||||
|
getFieldEditData(field: any, inputData: any, originalFieldData: any): any {
|
||||||
|
const fieldName = 'f_' + field.id;
|
||||||
|
|
||||||
|
const checkboxes = [];
|
||||||
|
inputData[fieldName].forEach((value, option) => {
|
||||||
|
if (value) {
|
||||||
|
checkboxes.push(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (checkboxes.length > 0) {
|
||||||
|
return [{
|
||||||
|
fieldid: field.id,
|
||||||
|
value: checkboxes
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field data in changed.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @param {any} originalFieldData Original field entered data.
|
||||||
|
* @return {Promise<boolean> | boolean} If the field has changes.
|
||||||
|
*/
|
||||||
|
hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise<boolean> | boolean {
|
||||||
|
const fieldName = 'f_' + field.id,
|
||||||
|
checkboxes = [];
|
||||||
|
|
||||||
|
inputData[fieldName].forEach((value, option) => {
|
||||||
|
if (value) {
|
||||||
|
checkboxes.push(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
originalFieldData = (originalFieldData && originalFieldData.content) || '';
|
||||||
|
|
||||||
|
return checkboxes.join('##') != originalFieldData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and get field requeriments.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @return {string | false} String with the notification or false.
|
||||||
|
*/
|
||||||
|
getFieldsNotifications(field: any, inputData: any): string | false {
|
||||||
|
if (field.required && (!inputData || !inputData.length || !inputData[0].value)) {
|
||||||
|
return this.translate.instant('addon.mod_data.errormustsupplyvalue');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override field content data with offline submission.
|
||||||
|
*
|
||||||
|
* @param {any} originalContent Original data to be overriden.
|
||||||
|
* @param {any} offlineContent Array with all the offline data to override.
|
||||||
|
* @param {any} [offlineFiles] Array with all the offline files in the field.
|
||||||
|
* @return {any} Data overriden
|
||||||
|
*/
|
||||||
|
overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any {
|
||||||
|
originalContent.content = (offlineContent[''] && offlineContent[''].join('##')) || '';
|
||||||
|
|
||||||
|
return originalContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
// (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 { AddonModDataFieldCheckboxModule } from './checkbox/checkbox.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
AddonModDataFieldCheckboxModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: []
|
||||||
|
})
|
||||||
|
export class AddonModDataFieldModule { }
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||||
|
|
||||||
|
<ion-buttons end>
|
||||||
|
<!-- The buttons defined by the component will be added in here. -->
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="dataComponent.loaded" (ionRefresh)="dataComponent.doRefresh($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<addon-mod-data-index [module]="module" [courseId]="courseId" [group]="group" (dataRetrieved)="updateData($event)"></addon-mod-data-index>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (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 { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModDataComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModDataIndexPage } from './index';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModDataIndexPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
AddonModDataComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModDataIndexPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModDataIndexPageModule {}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// (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 } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
import { AddonModDataIndexComponent } from '../../components/index/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays a data.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-data-index' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-data-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonModDataIndexPage {
|
||||||
|
@ViewChild(AddonModDataIndexComponent) dataComponent: AddonModDataIndexComponent;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
module: any;
|
||||||
|
courseId: number;
|
||||||
|
group: number;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams) {
|
||||||
|
this.module = navParams.get('module') || {};
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.group = navParams.get('group') || 0;
|
||||||
|
this.title = this.module.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update some data based on the data instance.
|
||||||
|
*
|
||||||
|
* @param {any} data Data instance.
|
||||||
|
*/
|
||||||
|
updateData(data: any): void {
|
||||||
|
this.title = data.name || this.title;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,573 @@
|
||||||
|
// (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 { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||||
|
import { AddonModDataOfflineProvider } from './offline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that provides some features for databases.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataProvider {
|
||||||
|
static COMPONENT = 'mmaModData';
|
||||||
|
static PER_PAGE = 25;
|
||||||
|
static ENTRY_CHANGED = 'addon_mod_data_entry_changed';
|
||||||
|
|
||||||
|
protected ROOT_CACHE_KEY = AddonModDataProvider.COMPONENT + ':';
|
||||||
|
protected logger;
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
|
||||||
|
private filepoolProvider: CoreFilepoolProvider, private dataOffline: AddonModDataOfflineProvider) {
|
||||||
|
this.logger = logger.getInstance('AddonModDataProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new entry to a database. It does not cache calls. It will fail if offline or cannot connect.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {any} data The fields data to be created.
|
||||||
|
* @param {number} [groupId] Group id, 0 means that the function will determine the user group.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the action is done.
|
||||||
|
*/
|
||||||
|
addEntryOnline(dataId: number, data: any, groupId?: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
databaseid: dataId,
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof groupId !== 'undefined') {
|
||||||
|
params['groupid'] = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.write('mod_data_add_entry', params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approves or unapproves an entry. It does not cache calls. It will fail if offline or cannot connect.
|
||||||
|
*
|
||||||
|
* @param {number} entryId Entry ID.
|
||||||
|
* @param {boolean} approve Whether to approve (true) or unapprove the entry.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the action is done.
|
||||||
|
*/
|
||||||
|
approveEntryOnline(entryId: number, approve: boolean, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
entryid: entryId,
|
||||||
|
approve: approve ? 1 : 0
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.write('mod_data_approve_entry', params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an entry. It does not cache calls. It will fail if offline or cannot connect.
|
||||||
|
*
|
||||||
|
* @param {number} entryId Entry ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the action is done.
|
||||||
|
*/
|
||||||
|
deleteEntryOnline(entryId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
entryid: entryId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.write('mod_data_delete_entry', params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect.
|
||||||
|
*
|
||||||
|
* @param {number} entryId Entry ID.
|
||||||
|
* @param {any} data The fields data to be updated.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the action is done.
|
||||||
|
*/
|
||||||
|
editEntryOnline(entryId: number, data: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
entryid: entryId,
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.write('mod_data_update_entry', params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for data data WS calls.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getDatabaseDataCacheKey(courseId: number): string {
|
||||||
|
return this.ROOT_CACHE_KEY + 'data:' + courseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get prefix cache key for all database activity data WS calls.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getDatabaseDataPrefixCacheKey(dataId: number): string {
|
||||||
|
return this.ROOT_CACHE_KEY + dataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a database data. If more than one is found, only the first will be returned.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {string} key Name of the property to check.
|
||||||
|
* @param {any} value Value to search.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is retrieved.
|
||||||
|
*/
|
||||||
|
protected getDatabaseByKey(courseId: number, key: string, value: any, siteId?: string, forceCache: boolean = false):
|
||||||
|
Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
courseids: [courseId]
|
||||||
|
},
|
||||||
|
preSets = {
|
||||||
|
cacheKey: this.getDatabaseDataCacheKey(courseId)
|
||||||
|
};
|
||||||
|
if (forceCache) {
|
||||||
|
preSets['omitExpires'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.read('mod_data_get_databases_by_courses', params, preSets).then((response) => {
|
||||||
|
if (response && response.databases) {
|
||||||
|
const currentData = response.databases.find((data) => data[key] == value);
|
||||||
|
if (currentData) {
|
||||||
|
return currentData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a data by course module ID.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {number} cmId Course module ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is retrieved.
|
||||||
|
*/
|
||||||
|
getDatabase(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise<any> {
|
||||||
|
return this.getDatabaseByKey(courseId, 'coursemodule', cmId, siteId, forceCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a data by ID.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {number} id Data ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is retrieved.
|
||||||
|
*/
|
||||||
|
getDatabaseById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise<any> {
|
||||||
|
return this.getDatabaseByKey(courseId, 'id', id, siteId, forceCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get prefix cache key for all database access information data WS calls.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getDatabaseAccessInformationDataPrefixCacheKey(dataId: number): string {
|
||||||
|
return this.getDatabaseDataPrefixCacheKey(dataId) + ':access:';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for database access information data WS calls.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @param {number} [groupId=0] Group ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getDatabaseAccessInformationDataCacheKey(dataId: number, groupId: number = 0): string {
|
||||||
|
return this.getDatabaseAccessInformationDataPrefixCacheKey(dataId) + groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access information for a given database.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @param {number} [groupId] Group ID.
|
||||||
|
* @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache.
|
||||||
|
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down).
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the database is retrieved.
|
||||||
|
*/
|
||||||
|
getDatabaseAccessInformation(dataId: number, groupId?: number, offline: boolean = false, ignoreCache: boolean = false,
|
||||||
|
siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
databaseid: dataId
|
||||||
|
},
|
||||||
|
preSets = {
|
||||||
|
cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, groupId)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof groupId !== 'undefined') {
|
||||||
|
params['groupid'] = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offline) {
|
||||||
|
preSets['omitExpires'] = true;
|
||||||
|
} else if (ignoreCache) {
|
||||||
|
preSets['getFromCache'] = false;
|
||||||
|
preSets['emergencyCache'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.read('mod_data_get_data_access_information', params, preSets);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entries for a specific database and group.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @param {number} [groupId=0] Group ID.
|
||||||
|
* @param {string} [sort=0] Sort the records by this field id, reserved ids are:
|
||||||
|
* 0: timeadded
|
||||||
|
* -1: firstname
|
||||||
|
* -2: lastname
|
||||||
|
* -3: approved
|
||||||
|
* -4: timemodified.
|
||||||
|
* Empty for using the default database setting.
|
||||||
|
* @param {string} [order=DESC] The direction of the sorting: 'ASC' or 'DESC'.
|
||||||
|
* Empty for using the default database setting.
|
||||||
|
* @param {number} [page=0] Page of records to return.
|
||||||
|
* @param {number} [perPage=PER_PAGE] Records per page to return. Default on PER_PAGE.
|
||||||
|
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||||
|
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down).
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the database is retrieved.
|
||||||
|
*/
|
||||||
|
getEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', page: number = 0,
|
||||||
|
perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false,
|
||||||
|
siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
// Always use sort and order params to improve cache usage (entries are identified by params).
|
||||||
|
const params = {
|
||||||
|
databaseid: dataId,
|
||||||
|
returncontents: 1,
|
||||||
|
page: page,
|
||||||
|
perpage: perPage,
|
||||||
|
groupid: groupId,
|
||||||
|
sort: sort,
|
||||||
|
order: order
|
||||||
|
},
|
||||||
|
preSets = {
|
||||||
|
cacheKey: this.getEntriesCacheKey(dataId, groupId)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (forceCache) {
|
||||||
|
preSets['omitExpires'] = true;
|
||||||
|
} else if (ignoreCache) {
|
||||||
|
preSets['getFromCache'] = false;
|
||||||
|
preSets['emergencyCache'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.read('mod_data_get_entries', params, preSets);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for database entries data WS calls.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @param {number} [groupId=0] Group ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getEntriesCacheKey(dataId: number, groupId: number = 0): string {
|
||||||
|
return this.getEntriesPrefixCacheKey(dataId) + groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get prefix cache key for database all entries data WS calls.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getEntriesPrefixCacheKey(dataId: number): string {
|
||||||
|
return this.getDatabaseDataPrefixCacheKey(dataId) + ':entries:';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an entry of the database activity.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID for caching purposes.
|
||||||
|
* @param {number} entryId Entry ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the database entry is retrieved.
|
||||||
|
*/
|
||||||
|
getEntry(dataId: number, entryId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
entryid: entryId,
|
||||||
|
returncontents: 1
|
||||||
|
},
|
||||||
|
preSets = {
|
||||||
|
cacheKey: this.getEntryCacheKey(dataId, entryId)
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.read('mod_data_get_entry', params, preSets);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for database entry data WS calls.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID for caching purposes.
|
||||||
|
* @param {number} entryId Entry ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getEntryCacheKey(dataId: number, entryId: number): string {
|
||||||
|
return this.getDatabaseDataPrefixCacheKey(dataId) + ':entry:' + entryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of configured fields for the given database.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||||
|
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the fields are retrieved.
|
||||||
|
*/
|
||||||
|
getFields(dataId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
databaseid: dataId
|
||||||
|
},
|
||||||
|
preSets = {
|
||||||
|
cacheKey: this.getFieldsCacheKey(dataId)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (forceCache) {
|
||||||
|
preSets['omitExpires'] = true;
|
||||||
|
} else if (ignoreCache) {
|
||||||
|
preSets['getFromCache'] = false;
|
||||||
|
preSets['emergencyCache'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.read('mod_data_get_fields', params, preSets).then((response) => {
|
||||||
|
if (response && response.fields) {
|
||||||
|
return response.fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for database fields data WS calls.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getFieldsCacheKey(dataId: number): string {
|
||||||
|
return this.getDatabaseDataPrefixCacheKey(dataId) + ':fields';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
* To invalidate files, use AddonDataProvider#invalidateFiles.
|
||||||
|
*
|
||||||
|
* @param {number} moduleId The module ID.
|
||||||
|
* @param {number} courseId Course ID of the module.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(this.getDatabase(courseId, moduleId).then((data) => {
|
||||||
|
const ps = [];
|
||||||
|
|
||||||
|
// Do not invalidate module data before getting module info, we need it!
|
||||||
|
ps.push(this.invalidateDatabaseData(courseId, siteId));
|
||||||
|
ps.push(this.invalidateDatabaseWSData(data.id, siteId));
|
||||||
|
|
||||||
|
return Promise.all(ps);
|
||||||
|
}));
|
||||||
|
|
||||||
|
promises.push(this.invalidateFiles(moduleId, siteId));
|
||||||
|
|
||||||
|
return this.utils.allPromises(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates database access information data.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateDatabaseAccessInformationData(dataId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.invalidateWsCacheForKeyStartingWith(this.getDatabaseAccessInformationDataPrefixCacheKey(dataId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates database entries data.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateEntriesData(dataId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.invalidateWsCacheForKeyStartingWith(this.getEntriesPrefixCacheKey(dataId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched files.
|
||||||
|
*
|
||||||
|
* @param {number} moduleId The module ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the files are invalidated.
|
||||||
|
*/
|
||||||
|
invalidateFiles(moduleId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModDataProvider.COMPONENT, moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates database data.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateDatabaseData(courseId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.invalidateWsCacheForKey(this.getDatabaseDataCacheKey(courseId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates database data except files and module info.
|
||||||
|
*
|
||||||
|
* @param {number} databaseId Data ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateDatabaseWSData(databaseId: number, siteId: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.invalidateWsCacheForKeyStartingWith(this.getDatabaseDataPrefixCacheKey(databaseId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the database WS are available.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<boolean>} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
|
||||||
|
* @since 3.3
|
||||||
|
*/
|
||||||
|
isPluginEnabled(siteId?: string): Promise<boolean> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.wsAvailable('mod_data_get_data_access_information');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report the database as being viewed.
|
||||||
|
*
|
||||||
|
* @param {number} id Module ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when the WS call is successful.
|
||||||
|
*/
|
||||||
|
logView(id: number): Promise<any> {
|
||||||
|
const params = {
|
||||||
|
databaseid: id
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.sitesProvider.getCurrentSite().write('mod_data_view_database', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs search over a database.
|
||||||
|
*
|
||||||
|
* @param {number} dataId The data instance id.
|
||||||
|
* @param {number} [groupId=0] Group id, 0 means that the function will determine the user group.
|
||||||
|
* @param {string} [search] Search text. It will be used if advSearch is not defined.
|
||||||
|
* @param {any} [advSearch] Advanced search data.
|
||||||
|
* @param {string} [sort] Sort by this field.
|
||||||
|
* @param {string} [order] The direction of the sorting.
|
||||||
|
* @param {number} [page=0] Page of records to return.
|
||||||
|
* @param {number} [perPage=PER_PAGE] Records per page to return. Default on AddonModDataProvider.PER_PAGE.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the action is done.
|
||||||
|
*/
|
||||||
|
searchEntries(dataId: number, groupId: number = 0, search?: string, advSearch?: any, sort?: string, order?: string,
|
||||||
|
page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
databaseid: dataId,
|
||||||
|
groupid: groupId,
|
||||||
|
returncontents: 1,
|
||||||
|
page: page,
|
||||||
|
perpage: perPage
|
||||||
|
},
|
||||||
|
preSets = {
|
||||||
|
getFromCache: false,
|
||||||
|
saveToCache: true,
|
||||||
|
emergencyCache: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof sort != 'undefined') {
|
||||||
|
params['sort'] = sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof order !== 'undefined') {
|
||||||
|
params['order'] = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof search !== 'undefined') {
|
||||||
|
params['search'] = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof advSearch !== 'undefined') {
|
||||||
|
params['advsearch'] = advSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.read('mod_data_search_entries', params, preSets);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
// (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 { AddonModDataFieldHandler } from './fields-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default handler used when a field plugin doesn't have a specific implementation.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataDefaultFieldHandler implements AddonModDataFieldHandler {
|
||||||
|
name = 'AddonModDataDefaultFieldHandler';
|
||||||
|
type = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field search data in the input data.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the search form.
|
||||||
|
* @return {any} With name and value of the data to be sent.
|
||||||
|
*/
|
||||||
|
getFieldSearchData(field: any, inputData: any): any {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field edit data in the input data.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @return {any} With name and value of the data to be sent.
|
||||||
|
*/
|
||||||
|
getFieldEditData(field: any, inputData: any, originalFieldData: any): any {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field data in changed.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @param {any} originalFieldData Original field entered data.
|
||||||
|
* @return {Promise<boolean> | boolean} If the field has changes.
|
||||||
|
*/
|
||||||
|
hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise<boolean> | boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field edit files in the input data.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field..
|
||||||
|
* @return {any} With name and value of the data to be sent.
|
||||||
|
*/
|
||||||
|
getFieldEditFiles(field: any, inputData: any, originalFieldData: any): any {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and get field requeriments.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @return {string | false} String with the notification or false.
|
||||||
|
*/
|
||||||
|
getFieldsNotifications(field: any, inputData: any): string | false {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override field content data with offline submission.
|
||||||
|
*
|
||||||
|
* @param {any} originalContent Original data to be overriden.
|
||||||
|
* @param {any} offlineContent Array with all the offline data to override.
|
||||||
|
* @param {any} [offlineFiles] Array with all the offline files in the field.
|
||||||
|
* @return {any} Data overriden
|
||||||
|
*/
|
||||||
|
overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any {
|
||||||
|
return originalContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
// (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 { Injector, Injectable } from '@angular/core';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||||
|
import { AddonModDataDefaultFieldHandler } from './default-field-handler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that all fields handlers must implement.
|
||||||
|
*/
|
||||||
|
export interface AddonModDataFieldHandler extends CoreDelegateHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the type of data field the handler supports. E.g. 'checkbox'.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Component to use to display the plugin data.
|
||||||
|
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||||
|
*
|
||||||
|
* @param {Injector} injector Injector.
|
||||||
|
* @param {any} field The field object.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent?(injector: Injector, plugin: any): any | Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field search data in the input data.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the search form.
|
||||||
|
* @return {any} With name and value of the data to be sent.
|
||||||
|
*/
|
||||||
|
getFieldSearchData?(field: any, inputData: any): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field edit data in the input data.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @return {any} With name and value of the data to be sent.
|
||||||
|
*/
|
||||||
|
getFieldEditData?(field: any, inputData: any, originalFieldData: any): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field data in changed.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @param {any} originalFieldData Original field entered data.
|
||||||
|
* @return {Promise<boolean> | boolean} If the field has changes.
|
||||||
|
*/
|
||||||
|
hasFieldDataChanged?(field: any, inputData: any, originalFieldData: any): Promise<boolean> | boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field edit files in the input data.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field..
|
||||||
|
* @return {any} With name and value of the data to be sent.
|
||||||
|
*/
|
||||||
|
getFieldEditFiles?(field: any, inputData: any, originalFieldData: any): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and get field requeriments.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @return {string | false} String with the notification or false.
|
||||||
|
*/
|
||||||
|
getFieldsNotifications?(field: any, inputData: any): string | false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override field content data with offline submission.
|
||||||
|
*
|
||||||
|
* @param {any} originalContent Original data to be overriden.
|
||||||
|
* @param {any} offlineContent Array with all the offline data to override.
|
||||||
|
* @param {any} [offlineFiles] Array with all the offline files in the field.
|
||||||
|
* @return {any} Data overriden
|
||||||
|
*/
|
||||||
|
overrideData?(originalContent: any, offlineContent: any, offlineFiles?: any): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate to register database fields handlers.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataFieldsDelegate extends CoreDelegate {
|
||||||
|
|
||||||
|
protected handlerNameProperty = 'type';
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
|
||||||
|
protected utils: CoreUtilsProvider, protected defaultHandler: AddonModDataDefaultFieldHandler) {
|
||||||
|
super('AddonModDataFieldsDelegate', logger, sitesProvider, eventsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the component to use for a certain field field.
|
||||||
|
*
|
||||||
|
* @param {Injector} injector Injector.
|
||||||
|
* @param {any} field The field object.
|
||||||
|
* @return {Promise<any>} Promise resolved with the component to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponentForField(injector: Injector, field: any): Promise<any> {
|
||||||
|
return Promise.resolve(this.executeFunctionOnEnabled(field.type, 'getComponent', [injector, field]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database data in the input data to search.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the search form.
|
||||||
|
* @return {any} Name and data field.
|
||||||
|
*/
|
||||||
|
getFieldSearchData(field: any, inputData: any): any {
|
||||||
|
return this.executeFunctionOnEnabled(field.type, 'getFieldSearchData', [field, inputData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database data in the input data to add or update entry.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the search form.
|
||||||
|
* @param {any} originalFieldData Original field entered data.
|
||||||
|
* @return {any} Name and data field.
|
||||||
|
*/
|
||||||
|
getFieldEditData(field: any, inputData: any, originalFieldData: any): any {
|
||||||
|
return this.executeFunctionOnEnabled(field.type, 'getFieldEditData', [field, inputData, originalFieldData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database data in the input files to add or update entry.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the search form.
|
||||||
|
* @param {any} originalFieldData Original field entered data.
|
||||||
|
* @return {any} Name and data field.
|
||||||
|
*/
|
||||||
|
getFieldEditFiles(field: any, inputData: any, originalFieldData: any): any {
|
||||||
|
return this.executeFunctionOnEnabled(field.type, 'getFieldEditFiles', [field, inputData, originalFieldData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and get field requeriments.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the edit form.
|
||||||
|
* @return {string} String with the notification or false.
|
||||||
|
*/
|
||||||
|
getFieldsNotifications(field: any, inputData: any): string {
|
||||||
|
return this.executeFunctionOnEnabled(field.type, 'getFieldsNotifications', [field, inputData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if field type manage files or not.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be checked.
|
||||||
|
* @return {boolean} If the field type manages files.
|
||||||
|
*/
|
||||||
|
hasFiles(field: any): boolean {
|
||||||
|
return this.hasFunction(field.type, 'getFieldEditFiles');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the data has changed for a certain field.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} inputData Data entered in the search form.
|
||||||
|
* @param {any} originalFieldData Original field entered data.
|
||||||
|
* @return {Promise<void>} Promise rejected if has changed, resolved if no changes.
|
||||||
|
*/
|
||||||
|
hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise<void> {
|
||||||
|
if (!this.hasFunction(field.type, 'hasFieldDataChanged')) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(this.executeFunctionOnEnabled(field.type, 'hasFieldDataChanged',
|
||||||
|
[field, inputData, originalFieldData])).then((result) => {
|
||||||
|
return result ? Promise.reject(null) : Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a field plugin is supported.
|
||||||
|
*
|
||||||
|
* @param {string} pluginType Type of the plugin.
|
||||||
|
* @return {boolean} True if supported, false otherwise.
|
||||||
|
*/
|
||||||
|
isPluginSupported(pluginType: string): boolean {
|
||||||
|
return this.hasHandler(pluginType, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override field content data with offline submission.
|
||||||
|
*
|
||||||
|
* @param {any} field Defines the field to be rendered.
|
||||||
|
* @param {any} originalContent Original data to be overriden.
|
||||||
|
* @param {any} offlineContent Array with all the offline data to override.
|
||||||
|
* @param {any} [offlineFiles] Array with all the offline files in the field.
|
||||||
|
* @return {any} Data overriden
|
||||||
|
*/
|
||||||
|
overrideData(field: any, originalContent: any, offlineContent: any, offlineFiles?: any): any {
|
||||||
|
if (!offlineContent || !this.hasFunction(field.type, 'overrideData')) {
|
||||||
|
return originalContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.executeFunctionOnEnabled(field.type, 'overrideData', [originalContent || {}, offlineContent, offlineFiles]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,348 @@
|
||||||
|
// (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 { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { AddonModDataFieldsDelegate } from './fields-delegate';
|
||||||
|
import { AddonModDataOfflineProvider } from './offline';
|
||||||
|
import { AddonModDataProvider } from './data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that provides helper functions for datas.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataHelperProvider {
|
||||||
|
|
||||||
|
constructor(private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
|
private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate,
|
||||||
|
private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the record with the offline actions applied.
|
||||||
|
*
|
||||||
|
* @param {any} record Entry to modify.
|
||||||
|
* @param {any} offlineActions Offline data with the actions done.
|
||||||
|
* @param {any} fields Entry defined fields indexed by fieldid.
|
||||||
|
* @return {any} Modified entry.
|
||||||
|
*/
|
||||||
|
applyOfflineActions(record: any, offlineActions: any[], fields: any[]): any {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
offlineActions.forEach((action) => {
|
||||||
|
switch (action.action) {
|
||||||
|
case 'approve':
|
||||||
|
record.approved = true;
|
||||||
|
break;
|
||||||
|
case 'disapprove':
|
||||||
|
record.approved = false;
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
record.deleted = true;
|
||||||
|
break;
|
||||||
|
case 'add':
|
||||||
|
case 'edit':
|
||||||
|
const offlineContents = {};
|
||||||
|
|
||||||
|
action.fields.forEach((offlineContent) => {
|
||||||
|
if (typeof offlineContents[offlineContent.fieldid] == 'undefined') {
|
||||||
|
offlineContents[offlineContent.fieldid] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offlineContent.subfield) {
|
||||||
|
offlineContents[offlineContent.fieldid][offlineContent.subfield] = JSON.parse(offlineContent.value);
|
||||||
|
} else {
|
||||||
|
offlineContents[offlineContent.fieldid][''] = JSON.parse(offlineContent.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Override field contents.
|
||||||
|
fields.forEach((field) => {
|
||||||
|
if (this.fieldsDelegate.hasFiles(field)) {
|
||||||
|
promises.push(this.getStoredFiles(record.dataid, record.id, field.id).then((offlineFiles) => {
|
||||||
|
record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id],
|
||||||
|
offlineContents[field.id], offlineFiles);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id],
|
||||||
|
offlineContents[field.id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
return record;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays Advanced Search Fields.
|
||||||
|
*
|
||||||
|
* @param {string} template Template HMTL.
|
||||||
|
* @param {any[]} fields Fields that defines every content in the entry.
|
||||||
|
* @return {string} Generated HTML.
|
||||||
|
*/
|
||||||
|
displayAdvancedSearchFields(template: string, fields: any[]): string {
|
||||||
|
if (!template) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let replace;
|
||||||
|
|
||||||
|
// Replace the fields found on template.
|
||||||
|
fields.forEach((field) => {
|
||||||
|
replace = '[[' + field.name + ']]';
|
||||||
|
replace = replace.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
|
||||||
|
replace = new RegExp(replace, 'gi');
|
||||||
|
|
||||||
|
// Replace field by a generic directive.
|
||||||
|
const render = '<addon-mod-data-field-plugin mode="search" field="fields[' + field.id +
|
||||||
|
']"></addon-mod-data-field-plugin>';
|
||||||
|
template = template.replace(replace, render);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Not pluginable other search elements.
|
||||||
|
// Replace firstname field by the text input.
|
||||||
|
replace = new RegExp('##fn##', 'gi');
|
||||||
|
let render = '<input type="text" name="firstname" placeholder="{{ \'addon.mod_data.authorfirstname\' | translate }}">';
|
||||||
|
template = template.replace(replace, render);
|
||||||
|
|
||||||
|
// Replace lastname field by the text input.
|
||||||
|
replace = new RegExp('##ln##', 'gi');
|
||||||
|
render = '<input type="text" name="lastname" placeholder="{{ \'addon.mod_data.authorlastname\' | translate }}">';
|
||||||
|
template = template.replace(replace, render);
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays fields for being shown.
|
||||||
|
*
|
||||||
|
* @param {string} template Template HMTL.
|
||||||
|
* @param {any[]} fields Fields that defines every content in the entry.
|
||||||
|
* @param {any} entry Entry.
|
||||||
|
* @param {string} mode Mode list or show.
|
||||||
|
* @param {any} actions Actions that can be performed to the record.
|
||||||
|
* @return {string} Generated HTML.
|
||||||
|
*/
|
||||||
|
displayShowFields(template: string, fields: any[], entry: any, mode: string, actions: any): string {
|
||||||
|
if (!template) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let replace, render;
|
||||||
|
|
||||||
|
// Replace the fields found on template.
|
||||||
|
fields.forEach((field) => {
|
||||||
|
replace = '[[' + field.name + ']]';
|
||||||
|
replace = replace.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
|
||||||
|
replace = new RegExp(replace, 'gi');
|
||||||
|
|
||||||
|
// Replace field by a generic directive.
|
||||||
|
render = '<addon-mod-data-field-plugin field="fields[' + field.id + ']" value="entries[' + entry.id + '].contents[' +
|
||||||
|
field.id + ']" mode="' + mode + '" database="data" (viewAction)="gotoEntry(' + entry.id +
|
||||||
|
')"></addon-mod-data-field-plugin>';
|
||||||
|
template = template.replace(replace, render);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const action in actions) {
|
||||||
|
replace = new RegExp('##' + action + '##', 'gi');
|
||||||
|
// Is enabled?
|
||||||
|
if (actions[action]) {
|
||||||
|
if (action == 'moreurl') {
|
||||||
|
// Render more url directly because it can be part of an HTML attribute.
|
||||||
|
render = this.sitesProvider.getCurrentSite().getURL() + '/mod/data/view.php?d={{data.id}}&rid=' + entry.id;
|
||||||
|
} else if (action == 'approvalstatus') {
|
||||||
|
render = this.translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved'));
|
||||||
|
} else {
|
||||||
|
render = '<addon-mod-data-action action="' + action + '" entry="entries[' + entry.id +
|
||||||
|
']" mode="' + mode + '" database="data"></addon-mod-data-action>';
|
||||||
|
}
|
||||||
|
template = template.replace(replace, render);
|
||||||
|
} else {
|
||||||
|
template = template.replace(replace, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object with all the actions that the user can do over the record.
|
||||||
|
*
|
||||||
|
* @param {any} database Database activity.
|
||||||
|
* @param {any} accessInfo Access info to the activity.
|
||||||
|
* @param {any} record Entry or record where the actions will be performed.
|
||||||
|
* @return {any} Keyed with the action names and boolean to evalute if it can or cannot be done.
|
||||||
|
*/
|
||||||
|
getActions(database: any, accessInfo: any, record: any): any {
|
||||||
|
return {
|
||||||
|
more: true,
|
||||||
|
moreurl: true,
|
||||||
|
user: true,
|
||||||
|
userpicture: true,
|
||||||
|
timeadded: true,
|
||||||
|
timemodified: true,
|
||||||
|
|
||||||
|
edit: record.canmanageentry && !record.deleted, // This already checks capabilities and readonly period.
|
||||||
|
delete: record.canmanageentry,
|
||||||
|
approve: database.approval && accessInfo.canapprove && !record.approved && !record.deleted,
|
||||||
|
disapprove: database.approval && accessInfo.canapprove && record.approved && !record.deleted,
|
||||||
|
|
||||||
|
approvalstatus: database.approval,
|
||||||
|
comments: database.comments,
|
||||||
|
|
||||||
|
// Unsupported actions.
|
||||||
|
delcheck: false,
|
||||||
|
export: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the entered data in search in a form.
|
||||||
|
* We don't use ng-model because it doesn't detect changes done by JavaScript.
|
||||||
|
*
|
||||||
|
* @param {any} form Form (DOM element).
|
||||||
|
* @param {any[]} fields Fields that defines every content in the entry.
|
||||||
|
* @return {any[]} Array with the answers.
|
||||||
|
*/
|
||||||
|
getSearchDataFromForm(form: any, fields: any[]): any[] {
|
||||||
|
if (!form || !form.elements) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchedData = this.domUtils.getDataFromForm(form);
|
||||||
|
|
||||||
|
// Filter and translate fields to each field plugin.
|
||||||
|
const advancedSearch = [];
|
||||||
|
fields.forEach((field) => {
|
||||||
|
const fieldData = this.fieldsDelegate.getFieldSearchData(field, searchedData);
|
||||||
|
|
||||||
|
if (fieldData) {
|
||||||
|
fieldData.forEach((data) => {
|
||||||
|
data.value = JSON.stringify(data.value);
|
||||||
|
// WS wants values in Json format.
|
||||||
|
advancedSearch.push(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Not pluginable other search elements.
|
||||||
|
if (searchedData['firstname']) {
|
||||||
|
// WS wants values in Json format.
|
||||||
|
advancedSearch.push({
|
||||||
|
name: 'firstname',
|
||||||
|
value: JSON.stringify(searchedData['firstname'])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchedData['lastname']) {
|
||||||
|
// WS wants values in Json format.
|
||||||
|
advancedSearch.push({
|
||||||
|
name: 'lastname',
|
||||||
|
value: JSON.stringify(searchedData['lastname'])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return advancedSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {number} entryId Entry ID or, if creating, timemodified.
|
||||||
|
* @param {number} fieldId Field ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the files.
|
||||||
|
*/
|
||||||
|
getStoredFiles(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.dataOffline.getEntryFieldFolder(dataId, entryId, fieldId, siteId).then((folderPath) => {
|
||||||
|
return this.fileUploaderProvider.getStoredFiles(folderPath).catch(() => {
|
||||||
|
// Ignore not found files.
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a prefix to all rules in a CSS string.
|
||||||
|
*
|
||||||
|
* @param {string} css CSS code to be prefixed.
|
||||||
|
* @param {string} prefix Prefix css selector.
|
||||||
|
* @return {string} Prefixed CSS.
|
||||||
|
*/
|
||||||
|
prefixCSS(css: string, prefix: string): string {
|
||||||
|
if (!css) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove comments first.
|
||||||
|
let regExp = /\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm;
|
||||||
|
css = css.replace(regExp, '');
|
||||||
|
// Add prefix.
|
||||||
|
regExp = /([^]*?)({[^]*?}|,)/g;
|
||||||
|
|
||||||
|
return css.replace(regExp, prefix + ' $1 $2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of files (either online files or local files), store the local files in a local folder
|
||||||
|
* to be submitted later.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {number} entryId Entry ID or, if creating, timemodified.
|
||||||
|
* @param {number} fieldId Field ID.
|
||||||
|
* @param {any[]} files List of files.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
storeFiles(dataId: number, entryId: number, fieldId: number, files: any[], siteId?: string): Promise<any> {
|
||||||
|
// Get the folder where to store the files.
|
||||||
|
return this.dataOffline.getEntryFieldFolder(dataId, entryId, fieldId, siteId).then((folderPath) => {
|
||||||
|
return this.fileUploaderProvider.storeFilesToUpload(folderPath, files);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload or store some files, depending if the user is offline or not.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {number} [itemId=0] Draft ID to use. Undefined or 0 to create a new draft ID.
|
||||||
|
* @param {number} entryId Entry ID or, if creating, timemodified.
|
||||||
|
* @param {number} fieldId Field ID.
|
||||||
|
* @param {any[]} files List of files.
|
||||||
|
* @param {boolean} offline True if files sould be stored for offline, false to upload them.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if success.
|
||||||
|
*/
|
||||||
|
uploadOrStoreFiles(dataId: number, itemId: number = 0, entryId: number, fieldId: number, files: any[], offline: boolean,
|
||||||
|
siteId?: string): Promise<any> {
|
||||||
|
if (files.length) {
|
||||||
|
if (offline) {
|
||||||
|
return this.storeFiles(dataId, entryId, fieldId, files, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fileUploaderProvider.uploadOrReuploadFiles(files, AddonModDataProvider.COMPONENT, itemId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
// (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 { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler';
|
||||||
|
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to data.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataLinkHandler extends CoreContentLinksModuleIndexHandler {
|
||||||
|
name = 'AddonModDataLinkHandler';
|
||||||
|
|
||||||
|
constructor(courseHelper: CoreCourseHelperProvider) {
|
||||||
|
super(courseHelper, AddonModDataLinkHandler.name, 'data');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
// (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, NavOptions } from 'ionic-angular';
|
||||||
|
import { AddonModDataIndexComponent } from '../components/index/index';
|
||||||
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { AddonModDataProvider } from './data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support data modules.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataModuleHandler implements CoreCourseModuleHandler {
|
||||||
|
name = 'AddonModData';
|
||||||
|
modName = 'data';
|
||||||
|
|
||||||
|
constructor(private courseProvider: CoreCourseProvider, private dataProvider: AddonModDataProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): Promise<boolean> {
|
||||||
|
return this.dataProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data required to display the module in the course contents view.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object.
|
||||||
|
* @param {number} courseId The course ID.
|
||||||
|
* @param {number} sectionId The section ID.
|
||||||
|
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
||||||
|
*/
|
||||||
|
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
||||||
|
return {
|
||||||
|
icon: this.courseProvider.getModuleIconSrc('data'),
|
||||||
|
title: module.name,
|
||||||
|
class: 'addon-mod_data-handler',
|
||||||
|
showDownloadButton: true,
|
||||||
|
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||||
|
navCtrl.push('AddonModDataIndexPage', {module: module, courseId: courseId}, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||||
|
* The component returned must implement CoreCourseModuleMainComponent.
|
||||||
|
*
|
||||||
|
* @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 {
|
||||||
|
return AddonModDataIndexComponent;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
// (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 { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreFileProvider } from '@providers/file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle Offline data.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataOfflineProvider {
|
||||||
|
|
||||||
|
protected logger;
|
||||||
|
|
||||||
|
// Variables for database.
|
||||||
|
protected SURVEY_TABLE = 'addon_mod_data_entry';
|
||||||
|
protected tablesSchema = [
|
||||||
|
{
|
||||||
|
name: this.SURVEY_TABLE,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'dataid',
|
||||||
|
type: 'INTEGER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'courseid',
|
||||||
|
type: 'INTEGER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'groupid',
|
||||||
|
type: 'INTEGER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'action',
|
||||||
|
type: 'TEXT'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'entryid',
|
||||||
|
type: 'INTEGER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fields',
|
||||||
|
type: 'TEXT'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timemodified',
|
||||||
|
type: 'INTEGER'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primaryKeys: ['dataid', 'entryid']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
|
||||||
|
private fileProvider: CoreFileProvider) {
|
||||||
|
this.logger = logger.getInstance('AddonModDataOfflineProvider');
|
||||||
|
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all the actions of an entry.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {number} entryId Database entry ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if deleted, rejected if failure.
|
||||||
|
*/
|
||||||
|
deleteAllEntryActions(dataId: number, entryId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.getEntryActions(dataId, entryId, siteId).then((actions) => {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
actions.forEach((action) => {
|
||||||
|
promises.push(this.deleteEntry(dataId, entryId, action.action, siteId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an stored entry.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {number} entryId Database entry Id.
|
||||||
|
* @param {string} action Action to be done
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if deleted, rejected if failure.
|
||||||
|
*/
|
||||||
|
deleteEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().deleteRecords(this.SURVEY_TABLE, {dataid: dataId, entryid: entryId, action: action});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the stored entry data from all the databases.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with entries.
|
||||||
|
*/
|
||||||
|
getAllEntries(siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getAllRecords(this.SURVEY_TABLE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the stored entry data from a certain database.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with entries.
|
||||||
|
*/
|
||||||
|
getDatabaseEntries(dataId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getRecords(this.SURVEY_TABLE, {dataid: dataId});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an all stored entry actions data.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {number} entryId Database entry Id.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with entry actions.
|
||||||
|
*/
|
||||||
|
getEntryActions(dataId: number, entryId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getRecords(this.SURVEY_TABLE, {dataid: dataId, entryid: entryId});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are offline entries to send.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with boolean: true if has offline answers, false otherwise.
|
||||||
|
*/
|
||||||
|
hasOfflineData(dataId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.getDatabaseEntries(dataId, siteId).then((entries) => {
|
||||||
|
return !!entries.length;
|
||||||
|
}).catch(() => {
|
||||||
|
// No offline data found, return false.
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the folder where to store files for offline files in a database.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<string>} Promise resolved with the path.
|
||||||
|
*/
|
||||||
|
protected getDatabaseFolder(dataId: number, siteId?: string): Promise<string> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
|
||||||
|
const siteFolderPath = this.fileProvider.getSiteFolder(site.getId()),
|
||||||
|
folderPath = 'offlinedatabase/' + dataId;
|
||||||
|
|
||||||
|
return this.textUtils.concatenatePaths(siteFolderPath, folderPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the folder where to store files for a new offline entry.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {number} entryId The ID of the entry.
|
||||||
|
* @param {number} fieldId Field ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<string>} Promise resolved with the path.
|
||||||
|
*/
|
||||||
|
getEntryFieldFolder(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise<string> {
|
||||||
|
return this.getDatabaseFolder(dataId, siteId).then((folderPath) => {
|
||||||
|
return this.textUtils.concatenatePaths(folderPath, entryId + '_' + fieldId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
// (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, Injector } from '@angular/core';
|
||||||
|
import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
|
||||||
|
import { AddonModDataProvider } from './data';
|
||||||
|
import { AddonModDataHelperProvider } from './helper';
|
||||||
|
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to prefetch databases.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
|
||||||
|
name = 'data';
|
||||||
|
component = AddonModDataProvider.COMPONENT;
|
||||||
|
updatesNames = /^configuration$|^.*files$|^entries$|^gradeitems$|^outcomes$|^comments$|^ratings/;
|
||||||
|
|
||||||
|
constructor(injector: Injector, protected dataProvider: AddonModDataProvider,
|
||||||
|
protected filepoolProvider: CoreFilepoolProvider, protected dataHelper: AddonModDataHelperProvider) {
|
||||||
|
super(injector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download or prefetch the content.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object returned by WS.
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {boolean} [prefetch] True to prefetch, false to download right away.
|
||||||
|
* @param {string} [dirPath] Path of the directory where to store all the content files. This is to keep the files
|
||||||
|
* relative paths and make the package work in an iframe. Undefined to download the files
|
||||||
|
* in the filepool root data.
|
||||||
|
* @return {Promise<any>} Promise resolved when all content is downloaded. Data returned is not reliable.
|
||||||
|
*/
|
||||||
|
downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(super.downloadOrPrefetch(module, courseId, prefetch));
|
||||||
|
promises.push(this.dataProvider.getDatabase(courseId, module.id).then((data) => {
|
||||||
|
// @TODO
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns data intro files.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object returned by WS.
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with list of intro files.
|
||||||
|
*/
|
||||||
|
getIntroFiles(module: any, courseId: number): Promise<any[]> {
|
||||||
|
return this.dataProvider.getDatabase(courseId, module.id).catch(() => {
|
||||||
|
// Not found, return undefined so module description is used.
|
||||||
|
}).then((data) => {
|
||||||
|
return this.getIntroFilesFromInstance(module, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param {number} moduleId The module ID.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||||
|
return this.dataProvider.invalidateContent(moduleId, courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate WS calls needed to determine module status.
|
||||||
|
*
|
||||||
|
* @param {any} module Module.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when invalidated.
|
||||||
|
*/
|
||||||
|
invalidateModule(module: any, courseId: number): Promise<any> {
|
||||||
|
return this.dataProvider.invalidateDatabaseData(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return this.dataProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// (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 { CoreCronHandler } from '@providers/cron';
|
||||||
|
import { AddonModDataSyncProvider } from './sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization cron handler.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataSyncCronHandler implements CoreCronHandler {
|
||||||
|
name = 'AddonModDataSyncCronHandler';
|
||||||
|
|
||||||
|
constructor(private dataSync: AddonModDataSyncProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the process.
|
||||||
|
* Receives the ID of the site affected, undefined for all sites.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] ID of the site affected, undefined for all sites.
|
||||||
|
* @return {Promise<any>} Promise resolved when done, rejected if failure.
|
||||||
|
*/
|
||||||
|
execute(siteId?: string): Promise<any> {
|
||||||
|
return this.dataSync.syncAllDatabases(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time between consecutive executions.
|
||||||
|
*
|
||||||
|
* @return {number} Time between consecutive executions (in ms).
|
||||||
|
*/
|
||||||
|
getInterval(): number {
|
||||||
|
return 600000; // 10 minutes.
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,337 @@
|
||||||
|
// (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 { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||||
|
import { CoreAppProvider } from '@providers/app';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { AddonModDataOfflineProvider } from './offline';
|
||||||
|
import { AddonModDataProvider } from './data';
|
||||||
|
import { AddonModDataHelperProvider } from './helper';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreSyncProvider } from '@providers/sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to sync databases.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
|
||||||
|
|
||||||
|
static AUTO_SYNCED = 'addon_mod_data_autom_synced';
|
||||||
|
protected componentTranslate: string;
|
||||||
|
|
||||||
|
constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
|
||||||
|
protected appProvider: CoreAppProvider, private dataOffline: AddonModDataOfflineProvider,
|
||||||
|
private eventsProvider: CoreEventsProvider, private dataProvider: AddonModDataProvider,
|
||||||
|
protected translate: TranslateService, private utils: CoreUtilsProvider, courseProvider: CoreCourseProvider,
|
||||||
|
syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider,
|
||||||
|
private dataHelper: AddonModDataHelperProvider) {
|
||||||
|
super('AddonModDataSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||||
|
this.componentTranslate = courseProvider.translateModuleName('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a database has data to synchronize.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<boolean>} Promise resolved with boolean: true if has data to sync, false otherwise.
|
||||||
|
*/
|
||||||
|
hasDataToSync(dataId: number, siteId?: string): Promise<boolean> {
|
||||||
|
return this.dataOffline.hasOfflineData(dataId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to synchronize all the databases in a certain site or in all sites.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
syncAllDatabases(siteId?: string): Promise<any> {
|
||||||
|
return this.syncOnSites('all databases', this.syncAllDatabasesFunc.bind(this), undefined, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync all pending databases on a site.
|
||||||
|
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @param {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
protected syncAllDatabasesFunc(siteId?: string): Promise<any> {
|
||||||
|
// Get all data answers pending to be sent in the site.
|
||||||
|
return this.dataOffline.getAllEntries(siteId).then((offlineActions) => {
|
||||||
|
const promises = {};
|
||||||
|
|
||||||
|
// Do not sync same database twice.
|
||||||
|
offlineActions.forEach((action) => {
|
||||||
|
if (typeof promises[action.dataid] != 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
promises[action.dataid] = this.syncDatabaseIfNeeded(action.dataid, siteId)
|
||||||
|
.then((result) => {
|
||||||
|
if (result && result.updated) {
|
||||||
|
// Sync done. Send event.
|
||||||
|
this.eventsProvider.trigger(AddonModDataSyncProvider.AUTO_SYNCED, {
|
||||||
|
dataId: action.dataid,
|
||||||
|
warnings: result.warnings
|
||||||
|
}, siteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Promises will be an object so, convert to an array first;
|
||||||
|
return Promise.all(this.utils.objectToArray(promises));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync a database only if a certain time has passed since the last time.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Database ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is synced or if it doesn't need to be synced.
|
||||||
|
*/
|
||||||
|
syncDatabaseIfNeeded(dataId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.isSyncNeeded(dataId, siteId).then((needed) => {
|
||||||
|
if (needed) {
|
||||||
|
return this.syncDatabase(dataId, siteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize a data.
|
||||||
|
*
|
||||||
|
* @param {number} dataId Data ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
|
||||||
|
*/
|
||||||
|
syncDatabase(dataId: number, siteId?: string): Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (this.isSyncing(dataId, siteId)) {
|
||||||
|
// There's already a sync ongoing for this data and user, return the promise.
|
||||||
|
return this.getOngoingSync(dataId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that data isn't blocked.
|
||||||
|
if (this.syncProvider.isBlocked(AddonModDataProvider.COMPONENT, dataId, siteId)) {
|
||||||
|
this.logger.debug(`Cannot sync database '${dataId}' because it is blocked.`);
|
||||||
|
|
||||||
|
return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Try to sync data '${dataId}' in site ${siteId}'`);
|
||||||
|
|
||||||
|
let courseId,
|
||||||
|
data;
|
||||||
|
const result = {
|
||||||
|
warnings: [],
|
||||||
|
updated: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get answers to be sent.
|
||||||
|
const syncPromise = this.dataOffline.getDatabaseEntries(dataId, siteId).catch(() => {
|
||||||
|
// No offline data found, return empty object.
|
||||||
|
return [];
|
||||||
|
}).then((offlineActions) => {
|
||||||
|
if (!offlineActions.length) {
|
||||||
|
// Nothing to sync.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.appProvider.isOnline()) {
|
||||||
|
// Cannot sync in offline.
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
courseId = offlineActions[0].courseid;
|
||||||
|
|
||||||
|
// Send the answers.
|
||||||
|
return this.dataProvider.getDatabaseById(courseId, dataId, siteId).then((database) => {
|
||||||
|
data = database;
|
||||||
|
|
||||||
|
const offlineEntries = {};
|
||||||
|
|
||||||
|
offlineActions.forEach((entry) => {
|
||||||
|
if (typeof offlineEntries[entry.entryid] == 'undefined') {
|
||||||
|
offlineEntries[entry.entryid] = [];
|
||||||
|
}
|
||||||
|
offlineEntries[entry.entryid].push(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
const promises = this.utils.objectToArray(offlineEntries).map((entryActions) => {
|
||||||
|
return this.syncEntry(data, entryActions, result, siteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}).then(() => {
|
||||||
|
if (result.updated) {
|
||||||
|
// Data has been sent to server. Now invalidate the WS calls.
|
||||||
|
return this.dataProvider.invalidateContent(data.cmid, courseId, siteId).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
// Sync finished, set sync time.
|
||||||
|
return this.setSyncTime(dataId, siteId);
|
||||||
|
}).then(() => {
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.addOngoingSync(dataId, syncPromise, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize an entry.
|
||||||
|
*
|
||||||
|
* @param {any} data Database.
|
||||||
|
* @param {any} entryActions Entry actions.
|
||||||
|
* @param {any} result Object with the result of the sync.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
protected syncEntry(data: any, entryActions: any[], result: any, siteId?: string): Promise<any> {
|
||||||
|
let discardError,
|
||||||
|
timePromise,
|
||||||
|
entryId = 0,
|
||||||
|
offlineId,
|
||||||
|
deleted = false;
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
// Sort entries by timemodified.
|
||||||
|
entryActions = entryActions.sort((a: any, b: any) => a.timemodified - b.timemodified);
|
||||||
|
|
||||||
|
entryId = entryActions[0].entryid;
|
||||||
|
|
||||||
|
if (entryId > 0) {
|
||||||
|
timePromise = this.dataProvider.getEntry(data.id, entryId, siteId).then((entry) => {
|
||||||
|
return entry.entry.timemodified;
|
||||||
|
}).catch(() => {
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
offlineId = entryId;
|
||||||
|
timePromise = Promise.resolve(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timePromise.then((timemodified) => {
|
||||||
|
if (timemodified < 0 || timemodified >= entryActions[0].timemodified) {
|
||||||
|
// The entry was not found in Moodle or the entry has been modified, discard the action.
|
||||||
|
result.updated = true;
|
||||||
|
discardError = this.translate.instant('addon.mod_data.warningsubmissionmodified');
|
||||||
|
|
||||||
|
return this.dataOffline.deleteAllEntryActions(data.id, entryId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
entryActions.forEach((action) => {
|
||||||
|
let actionPromise;
|
||||||
|
const proms = [];
|
||||||
|
|
||||||
|
entryId = action.entryid > 0 ? action.entryid : entryId;
|
||||||
|
|
||||||
|
if (action.fields) {
|
||||||
|
action.fields.forEach((field) => {
|
||||||
|
// Upload Files if asked.
|
||||||
|
const value = JSON.parse(field.value);
|
||||||
|
if (value.online || value.offline) {
|
||||||
|
let files = value.online || [];
|
||||||
|
const fileProm = value.offline ? this.dataHelper.getStoredFiles(action.dataid, entryId, field.fieldid) :
|
||||||
|
Promise.resolve([]);
|
||||||
|
|
||||||
|
proms.push(fileProm.then((offlineFiles) => {
|
||||||
|
files = files.concat(offlineFiles);
|
||||||
|
|
||||||
|
return this.dataHelper.uploadOrStoreFiles(action.dataid, 0, entryId, field.fieldid, files, false,
|
||||||
|
siteId).then((filesResult) => {
|
||||||
|
field.value = JSON.stringify(filesResult);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
actionPromise = Promise.all(proms).then(() => {
|
||||||
|
// Perform the action.
|
||||||
|
switch (action.action) {
|
||||||
|
case 'add':
|
||||||
|
return this.dataProvider.addEntryOnline(action.dataid, action.fields, data.groupid, siteId)
|
||||||
|
.then((result) => {
|
||||||
|
entryId = result.newentryid;
|
||||||
|
});
|
||||||
|
case 'edit':
|
||||||
|
return this.dataProvider.editEntryOnline(entryId, action.fields, siteId);
|
||||||
|
case 'approve':
|
||||||
|
return this.dataProvider.approveEntryOnline(entryId, true, siteId);
|
||||||
|
case 'disapprove':
|
||||||
|
return this.dataProvider.approveEntryOnline(entryId, false, siteId);
|
||||||
|
case 'delete':
|
||||||
|
return this.dataProvider.deleteEntryOnline(entryId, siteId).then(() => {
|
||||||
|
deleted = true;
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
promises.push(actionPromise.catch((error) => {
|
||||||
|
if (error && error.wserror) {
|
||||||
|
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||||
|
discardError = error.error;
|
||||||
|
} else {
|
||||||
|
// Couldn't connect to server, reject.
|
||||||
|
return Promise.reject(error && error.error);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
// Delete the offline data.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
return this.dataOffline.deleteEntry(action.dataid, action.entryid, action.action, siteId);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}).then(() => {
|
||||||
|
if (discardError) {
|
||||||
|
// Submission was discarded, add a warning.
|
||||||
|
const message = this.translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: data.name,
|
||||||
|
error: discardError
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.warnings.indexOf(message) == -1) {
|
||||||
|
result.warnings.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync done. Send event.
|
||||||
|
this.eventsProvider.trigger(AddonModDataSyncProvider.AUTO_SYNCED, {
|
||||||
|
dataId: data.id,
|
||||||
|
entryId: entryId,
|
||||||
|
offlineEntryId: offlineId,
|
||||||
|
warnings: result.warnings,
|
||||||
|
deleted: deleted
|
||||||
|
}, siteId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -139,7 +139,7 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider {
|
||||||
let courseId,
|
let courseId,
|
||||||
feedback;
|
feedback;
|
||||||
|
|
||||||
this.logger.debug(`Try to sync feedback '${feedbackId}'`);
|
this.logger.debug(`Try to sync feedback '${feedbackId}' in site ${siteId}'`);
|
||||||
|
|
||||||
// Get offline responses to be sent.
|
// Get offline responses to be sent.
|
||||||
const syncPromise = this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).catch(() => {
|
const syncPromise = this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).catch(() => {
|
||||||
|
|
|
@ -81,6 +81,7 @@ import { AddonModAssignModule } from '@addon/mod/assign/assign.module';
|
||||||
import { AddonModBookModule } from '@addon/mod/book/book.module';
|
import { AddonModBookModule } from '@addon/mod/book/book.module';
|
||||||
import { AddonModChatModule } from '@addon/mod/chat/chat.module';
|
import { AddonModChatModule } from '@addon/mod/chat/chat.module';
|
||||||
import { AddonModChoiceModule } from '@addon/mod/choice/choice.module';
|
import { AddonModChoiceModule } from '@addon/mod/choice/choice.module';
|
||||||
|
import { AddonModDataModule } from '@addon/mod/data/data.module';
|
||||||
import { AddonModLabelModule } from '@addon/mod/label/label.module';
|
import { AddonModLabelModule } from '@addon/mod/label/label.module';
|
||||||
import { AddonModLtiModule } from '@addon/mod/lti/lti.module';
|
import { AddonModLtiModule } from '@addon/mod/lti/lti.module';
|
||||||
import { AddonModResourceModule } from '@addon/mod/resource/resource.module';
|
import { AddonModResourceModule } from '@addon/mod/resource/resource.module';
|
||||||
|
@ -185,6 +186,7 @@ export const CORE_PROVIDERS: any[] = [
|
||||||
AddonModBookModule,
|
AddonModBookModule,
|
||||||
AddonModChatModule,
|
AddonModChatModule,
|
||||||
AddonModChoiceModule,
|
AddonModChoiceModule,
|
||||||
|
AddonModDataModule,
|
||||||
AddonModLabelModule,
|
AddonModLabelModule,
|
||||||
AddonModResourceModule,
|
AddonModResourceModule,
|
||||||
AddonModFeedbackModule,
|
AddonModFeedbackModule,
|
||||||
|
|
|
@ -164,6 +164,20 @@ export class CoreDelegate {
|
||||||
return enabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName];
|
return enabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if function exists on a handler.
|
||||||
|
*
|
||||||
|
* @param {string} handlerName The handler name.
|
||||||
|
* @param {string} fnName Name of the function to execute.
|
||||||
|
* @param {booealn} [onlyEnabled=true] If check only enabled handlers or all.
|
||||||
|
* @return {any} Function returned value or default value.
|
||||||
|
*/
|
||||||
|
protected hasFunction(handlerName: string, fnName: string, onlyEnabled: boolean = true): any {
|
||||||
|
const handler = onlyEnabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName];
|
||||||
|
|
||||||
|
return handler && handler[fnName];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a handler name has a registered handler (not necessarily enabled).
|
* Check if a handler name has a registered handler (not necessarily enabled).
|
||||||
*
|
*
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injector } from '@angular/core';
|
import { Injector, Input } from '@angular/core';
|
||||||
import { Content } from 'ionic-angular';
|
import { Content } from 'ionic-angular';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
@ -26,6 +26,8 @@ import { CoreCourseModuleMainResourceComponent } from './main-resource-component
|
||||||
* Template class to easily create CoreCourseModuleMainComponent of activities.
|
* Template class to easily create CoreCourseModuleMainComponent of activities.
|
||||||
*/
|
*/
|
||||||
export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainResourceComponent {
|
export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainResourceComponent {
|
||||||
|
@Input() group?: number; // Group ID the component belongs to.
|
||||||
|
|
||||||
moduleName: string; // Raw module name to be translated. It will be translated on init.
|
moduleName: string; // Raw module name to be translated. It will be translated on init.
|
||||||
|
|
||||||
// Data for context menu.
|
// Data for context menu.
|
||||||
|
|
Loading…
Reference in New Issue