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,
|
||||
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.
|
||||
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 { AddonModChatModule } from '@addon/mod/chat/chat.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 { AddonModLtiModule } from '@addon/mod/lti/lti.module';
|
||||
import { AddonModResourceModule } from '@addon/mod/resource/resource.module';
|
||||
|
@ -185,6 +186,7 @@ export const CORE_PROVIDERS: any[] = [
|
|||
AddonModBookModule,
|
||||
AddonModChatModule,
|
||||
AddonModChoiceModule,
|
||||
AddonModDataModule,
|
||||
AddonModLabelModule,
|
||||
AddonModResourceModule,
|
||||
AddonModFeedbackModule,
|
||||
|
|
|
@ -164,6 +164,20 @@ export class CoreDelegate {
|
|||
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).
|
||||
*
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injector } from '@angular/core';
|
||||
import { Injector, Input } from '@angular/core';
|
||||
import { Content } from 'ionic-angular';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
|
||||
// Data for context menu.
|
||||
|
|
Loading…
Reference in New Issue