MOBILE-2338 data: Implement index page

main
Pau Ferrer Ocaña 2018-04-12 14:56:51 +02:00
parent a4b1dd0c73
commit bbc6fcdff5
31 changed files with 3258 additions and 2 deletions

View File

@ -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() { }
}

View File

@ -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 {}

View File

@ -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>

View File

@ -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', []));
}
}

View File

@ -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>

View File

@ -0,0 +1,3 @@
addon-mod-data-index {
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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 { }

View File

@ -0,0 +1,3 @@
{
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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);
});
}
}

View File

@ -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;
}
}

View File

@ -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]);
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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);
});
}
}

View File

@ -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();
}
}

View File

@ -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.
}
}

View File

@ -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);
});
}
}

View File

@ -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(() => {

View File

@ -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,

View File

@ -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).
*

View File

@ -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.