Merge pull request #1327 from crazyserver/MOBILE-2338

Mobile 2338
main
Juan Leyva 2018-05-30 17:09:34 +02:00 committed by GitHub
commit 7ef4726bf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
107 changed files with 9073 additions and 101 deletions

View File

@ -13,6 +13,10 @@
<allow-navigation href="data:*" />
<allow-navigation href="*" />
<allow-intent href="*" />
<allow-intent href="tel:*" />
<allow-intent href="sms:*" />
<allow-intent href="mailto:*" />
<allow-intent href="geo:*" />
<preference name="orientation" value="default" />
<preference name="target-device" value="universal" />
<preference name="fullscreen" value="false" />

View File

@ -153,7 +153,7 @@
<!-- Numeric grade. -->
<ion-item text-wrap *ngIf="grade.method == 'simple' && !grade.scale">
<ion-label stacked>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo.grade} }}</ion-label>
<ion-input type="number" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo.grade" [lang]="grade.lang" core-input-errors></ion-input>
<ion-input type="number" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo.grade" [lang]="grade.lang"></ion-input>
</ion-item>
<!-- Grade using a scale. -->

View File

@ -0,0 +1,90 @@
// (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, OnInit, OnChanges, SimpleChange } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
/**
* Base class for component to render a field.
*/
export class AddonModDataFieldPluginComponent implements OnInit, OnChanges {
@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.
@Input() form?: FormGroup; // Form where to add the form control. Just required for edit and search modes.
@Input() search?: any; // The search value of all fields.
constructor(protected fb: FormBuilder) { }
/**
* Add the form control for the search mode.
*
* @param {string} fieldName Control field name.
* @param {any} value Initial set value.
*/
protected addControl(fieldName: string, value?: any): void {
if (!this.form) {
return;
}
if (this.mode == 'search') {
this.form.addControl(fieldName, this.fb.control(this.search[fieldName] || null));
}
if (this.mode == 'edit') {
this.form.addControl(fieldName, this.fb.control(value, this.field.required ? Validators.required : null));
}
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.init();
}
/**
* Initialize field.
*/
protected init(): void {
return;
}
/**
* Return if is shown or list mode.
*
* @return {boolean} True if mode is show or list.
*/
isShowOrListMode(): boolean {
return this.mode == 'list' || this.mode == 'show';
}
/**
* Component being changed.
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (this.isShowOrListMode() && changes.value) {
this.updateValue(changes.value.currentValue);
}
}
/**
* Update value being shown.
*/
protected updateValue(value: any): void {
this.value = value;
}
}

View File

@ -0,0 +1,34 @@
<a *ngIf="action == 'more'" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'addon.mod_data.more' | translate">
<ion-icon name="search"></ion-icon>
</a>
<a *ngIf="action == 'edit'" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'core.edit' | translate">
<ion-icon name="cog"></ion-icon>
</a>
<a *ngIf="action == 'delete' && !entry.deleted" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'core.delete' | translate">
<ion-icon name="trash"></ion-icon>
</a>
<a *ngIf="action == 'delete' && entry.deleted" ion-button icon-only clear (click)="undoDelete()" [title]="'core.restore' | translate">
<ion-icon name="undo"></ion-icon>
</a>
<a *ngIf="action == 'approve'" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'addon.mod_data.approve' | translate">
<ion-icon name="thumbs-up"></ion-icon>
</a>
<a *ngIf="action == 'disapprove'" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'addon.mod_data.disapprove' | translate">
<ion-icon name="thumbs-down"></ion-icon>
</a>
<core-comments *ngIf="action == 'comments' && mode == 'list'" contextLevel="module" [instanceId]="database.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry"></core-comments>
<span *ngIf="action == 'timeadded'">{{ entry.timecreated * 1000 | coreFormatDate:"dffulldate" }}</span>
<span *ngIf="action == 'timemodified'">{{ entry.timemodified * 1000 | coreFormatDate:"dffulldate" }}</span>
<a *ngIf="action == 'userpicture'" core-user-link [courseId]="database.courseid" [userId]="entry.userid" [title]="entry.fullname">
<img class="avatar-round" [src]="userPicture" [alt]="'core.pictureof' | translate:{$a: user.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'" role="presentation">
</a>
<a *ngIf="action == 'user'" core-user-link [courseId]="database.courseid" [userId]="entry.userid" [title]="entry.fullname">{{entry.fullname}}</a>

View File

@ -0,0 +1,92 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit, Injector } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { AddonModDataProvider } from '../../providers/data';
import { AddonModDataOfflineProvider } from '../../providers/offline';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUserProvider } from '@core/user/providers/user';
/**
* Component that displays a database action.
*/
@Component({
selector: 'addon-mod-data-action',
templateUrl: 'action.html',
})
export class AddonModDataActionComponent implements OnInit {
@Input() mode: string; // The render mode.
@Input() action: string; // The field to render.
@Input() entry?: any; // The value of the field.
@Input() database: any; // Database object.
siteId: string;
rootUrl: string;
url: string;
userPicture: string;
constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider,
protected dataOffline: AddonModDataOfflineProvider, protected eventsProvider: CoreEventsProvider,
sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider) {
this.rootUrl = sitesProvider.getCurrentSite().getURL();
this.siteId = sitesProvider.getCurrentSiteId();
}
/**
* Undo delete action.
*
* @return {Promise<any>} Solved when done.
*/
undoDelete(): Promise<any> {
const dataId = this.database.id,
entryId = this.entry.id;
return this.dataOffline.getEntry(dataId, entryId, 'delete', this.siteId).then(() => {
// Found. Just delete the action.
return this.dataOffline.deleteEntry(dataId, entryId, 'delete', this.siteId);
}).then(() => {
this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, this.siteId);
});
}
/**
* Component being initialized.
*/
ngOnInit(): void {
switch (this.action) {
case 'more':
this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id;
break;
case 'edit':
this.url = this.rootUrl + '/mod/data/edit.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id;
break;
case 'delete':
this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&delete=' + this.entry.id;
break;
case 'approve':
this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&approve=' + this.entry.id;
break;
case 'disapprove':
this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&disapprove=' + this.entry.id;
break;
case 'userpicture':
this.userProvider.getProfile(this.entry.userid, this.database.courseid).then((profile) => {
this.userPicture = profile.profileimageurl;
});
break;
default:
break;
}
}
}

View File

@ -0,0 +1,57 @@
// (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 { CorePipesModule } from '@pipes/pipes.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { AddonModDataIndexComponent } from './index/index';
import { AddonModDataFieldPluginComponent } from './field-plugin/field-plugin';
import { AddonModDataActionComponent } from './action/action';
import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module';
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
@NgModule({
declarations: [
AddonModDataIndexComponent,
AddonModDataFieldPluginComponent,
AddonModDataActionComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
CoreCourseComponentsModule,
CoreCompileHtmlComponentModule,
CoreCommentsComponentsModule
],
providers: [
],
exports: [
AddonModDataIndexComponent,
AddonModDataFieldPluginComponent,
AddonModDataActionComponent
],
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,92 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit, Injector, ViewChild, OnChanges, SimpleChange } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AddonModDataProvider } from '../../providers/data';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
/**
* Component that displays a database field plugin.
*/
@Component({
selector: 'addon-mod-data-field-plugin',
templateUrl: 'field-plugin.html',
})
export class AddonModDataFieldPluginComponent implements OnInit, OnChanges {
@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.
@Input() form?: FormGroup; // Form where to add the form control. Just required for edit and search modes.
@Input() search?: any; // The search value of all fields.
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 {
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,
form: this.form,
search: this.search
};
}
}).finally(() => {
this.fieldLoaded = true;
});
}
/**
* Component being changed.
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (this.fieldLoaded && this.data) {
if (this.mode == 'edit' && changes.error) {
this.data.error = changes.error.currentValue;
}
if ((this.mode == 'show' || this.mode == 'list') && changes.value) {
this.data.value = changes.value.currentValue;
}
}
}
}

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="addon-data-contents addon-data-entries-{{data.id}}" *ngIf="!isEmpty">
<style *ngIf="cssTemplate">
{{ cssTemplate }}
</style>
<core-compile-html [text]="entriesRendered" [jsData]="jsData" [extraImports]="extraImports"></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,494 @@
// (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, NavController } 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 { AddonModDataComponentsModule } from '../components.module';
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;
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 = '';
extraImports = [AddonModDataComponentsModule];
jsData;
protected syncEventName = AddonModDataSyncProvider.AUTO_SYNCED;
protected entryChangedObserver: any;
protected hasComments = false;
protected fieldsArray: any;
constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider,
private dataOffline: AddonModDataOfflineProvider, @Optional() content: Content,
private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider,
private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider,
private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController) {
super(injector, content);
// 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(() => {
if (!this.data) {
return;
}
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 = this.utils.arrayToObject(fields, 'id');
this.fieldsArray = this.utils.objectToArray(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.fieldsArray));
} else {
promises.push(Promise.resolve(entry));
}
}
});
entries.entries.forEach((entry) => {
// Index contents by fieldid.
entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid');
if (typeof this.offlineActions[entry.id] != 'undefined') {
promises.push(this.dataHelper.applyOfflineActions(entry, this.offlineActions[entry.id], this.fieldsArray));
} 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.fieldsArray, entry, 'list',
actions);
});
entriesHTML += this.data.listtemplatefooter || '';
this.entriesRendered = entriesHTML;
// Pass the input data to the component.
this.jsData = {
fields: this.fields,
entries: this.entries,
data: this.data
};
});
} 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', {
search: this.search,
fields: this.fields,
data: this.data});
modal.onDidDismiss((data) => {
// Add data to search object.
if (data) {
this.search = data;
this.searchEntries(0);
}
});
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;
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.
*
* @param {number} groupId Group ID.
* @return {Promise<any>} Resolved when new group is selected or rejected if not.
*/
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);
});
}
/**
* Opens add entries form.
*/
gotoAddEntries(): void {
const params = {
module: this.module,
courseId: this.courseId,
group: this.selectedGroup
};
this.navCtrl.push('AddonModDataEditPage', params);
}
/**
* Goto the selected entry.
*
* @param {number} entryId Entry ID.
*/
gotoEntry(entryId: number): void {
const params = {
module: this.module,
courseId: this.courseId,
entryId: entryId,
group: this.selectedGroup
};
this.navCtrl.push('AddonModDataEntryPage', params);
}
/**
* 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,77 @@
// (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 { AddonModDataApproveLinkHandler } from './providers/approve-link-handler';
import { AddonModDataDeleteLinkHandler } from './providers/delete-link-handler';
import { AddonModDataShowLinkHandler } from './providers/show-link-handler';
import { AddonModDataEditLinkHandler } from './providers/edit-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,
AddonModDataApproveLinkHandler,
AddonModDataDeleteLinkHandler,
AddonModDataShowLinkHandler,
AddonModDataEditLinkHandler,
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,
approveLinkHandler: AddonModDataApproveLinkHandler, deleteLinkHandler: AddonModDataDeleteLinkHandler,
showLinkHandler: AddonModDataShowLinkHandler, editLinkHandler: AddonModDataEditLinkHandler) {
moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler);
contentLinksDelegate.registerHandler(linkHandler);
contentLinksDelegate.registerHandler(approveLinkHandler);
contentLinksDelegate.registerHandler(deleteLinkHandler);
contentLinksDelegate.registerHandler(showLinkHandler);
contentLinksDelegate.registerHandler(editLinkHandler);
cronDelegate.register(syncHandler);
}
}

View File

@ -0,0 +1,101 @@
.addon-data-contents {
overflow: visible;
white-space: normal;
word-break: break-word;
padding: $content-padding;
background-color: white;
border-top-width: 1px;
border-bottom-width: 1px;
border-right-width: 0;
border-left-width: 0;
border-style: solid;
border-color: $list-border-color;
table, tbody {
display: block;
}
tr {
@extend .row;
padding: 0;
}
td, th {
@extend .col;
}
}
page-addon-mod-data-search,
page-addon-mod-data-edit {
table {
width: 100%;
}
td {
vertical-align: top;
}
.item.item-input.item-block .item-inner ion-input,
.item.item-input.item-input-has-focus .item-inner ion-input,
.item.item-input.input-has-focus .item-inner ion-input {
border: 0 !important;
box-shadow: none;
}
.addon-data-lantlong {
display: flex;
}
form, .addon-data-advanced-search {
background-color: $list-background-color;
.core-mark-required {
float: right;
+ ion-input,
+ ion-select {
padding-right: 20px;
}
}
@if ($text-input-md-show-focus-highlight) {
.input-md input:focus {
@include md-input-highlight($text-input-md-highlight-color);
}
}
.input-md input {
@include padding-horizontal(null, ($item-md-padding-end / 2));
border-bottom: 1px solid $list-md-border-color;
&:focus {
@include md-input-highlight($text-input-md-highlight-color);
}
}
.input-ios input {
@include padding-horizontal(null, $item-ios-padding-end / 2);
@include safe-area-padding-horizontal(null, $item-ios-padding-end / 2);
border-bottom: $hairlines-width solid $list-ios-border-color;
&:focus {
@include ios-input-highlight($text-input-ios-highlight-color);
}
}
.input-wp input {
@include padding-horizontal(null, ($item-wp-padding-end / 2));
border-bottom: 1px solid $list-wp-border-color;
&:focus {
border-color: $text-input-wp-highlight-color;
}
}
ion-select {
width: 100%;
left: 0;
max-width: none;
}
.core-item-has-rich-text-editor {
margin-right: 1px;
}
}
}

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,15 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-select [formControlName]="'f_'+field.id" multiple="true" [placeholder]="'addon.mod_data.menuchoose' | translate" [selectOptions]="{title: field.name}" interface="popover">
<ion-option *ngFor="let option of options" [value]="option.value">{{option.key}}</ion-option>
</ion-select>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
<ion-item *ngIf="mode == 'search'">
<ion-label>{{ 'addon.mod_data.selectedrequired' | translate }}</ion-label>
<ion-checkbox item-end [formControlName]="'f_'+field.id+'_allreq'" [(ngModel)]="search['f_'+field.id+'_allreq']">
</ion-checkbox>
</ion-item>
</span>
<core-format-text *ngIf="isShowOrListMode() && value && value.content" [text]="value.content"></core-format-text>

View File

@ -0,0 +1,73 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
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 {
options = [];
constructor(protected fb: FormBuilder) {
super(fb);
}
/**
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
this.updateValue(this.value);
return;
}
this.options = this.field.param1.split('\n').map((option) => {
return { key: option, value: option };
});
const values = [];
if (this.mode == 'edit' && this.value && this.value.content) {
this.value.content.split('##').forEach((value) => {
const x = this.options.findIndex((option) => value == option.key);
if (x >= 0) {
values.push(value);
}
});
}
if (this.mode == 'search') {
this.addControl('f_' + this.field.id + '_allreq');
}
this.addControl('f_' + this.field.id, values);
}
/**
* Update value being shown.
*
* @param {any} value New value to be set.
*/
protected updateValue(value: any): void {
this.value = value;
this.value.content = value && value.content && value.content.split('##').join('<br>');
}
}

View File

@ -0,0 +1,146 @@
// (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> {
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 values = [];
if (inputData[fieldName] && inputData[fieldName].length > 0) {
values.push({
name: fieldName,
value: inputData[fieldName]
});
if (inputData[reqName]) {
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;
if (inputData[fieldName] && inputData[fieldName].length > 0) {
return [{
fieldid: field.id,
value: inputData[fieldName]
}];
}
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;
originalFieldData = (originalFieldData && originalFieldData.content) || '';
return inputData[fieldName].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,14 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-datetime [formControlName]="'f_'+field.id" [placeholder]="'core.date' | translate" [disabled]="mode == 'search' && !search['f_'+field.id+'_z']" [displayFormat]="format"></ion-datetime>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
<ion-item *ngIf="mode == 'search'">
<ion-label>{{ 'addon.mod_data.usedate' | translate }}</ion-label>
<ion-checkbox item-end [formControlName]="'f_'+field.id+'_z'" [(ngModel)]="search['f_'+field.id+'_z']">
</ion-checkbox>
</ion-item>
</span>
<core-format-text *ngIf="isShowOrListMode() && value && value.content" [text]="value.content * 1000 | coreFormatDate:'LL'"></core-format-text>

View File

@ -0,0 +1,58 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data date field.
*/
@Component({
selector: 'addon-mod-data-field-date',
templateUrl: 'date.html'
})
export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginComponent {
format: string;
constructor(protected fb: FormBuilder, protected timeUtils: CoreTimeUtilsProvider) {
super(fb);
}
/**
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
return;
}
let val;
this.format = this.timeUtils.getLocalizedDateFormat('LL');
if (this.mode == 'search') {
this.addControl('f_' + this.field.id + '_z');
val = this.search['f_' + this.field.id + '_y'] ? new Date(this.search['f_' + this.field.id + '_y'] + '-' +
this.search['f_' + this.field.id + '_m'] + '-' + this.search['f_' + this.field.id + '_d']) : new Date();
this.search['f_' + this.field.id] = val.toISOString();
} else {
val = this.value && this.value.content ? new Date(parseInt(this.value.content, 10) * 1000) : new Date();
val = val.toISOString();
}
this.addControl('f_' + this.field.id, val);
}
}

View File

@ -0,0 +1,51 @@
// (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 { AddonModDataFieldDateHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldDateComponent } from './component/date';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
@NgModule({
declarations: [
AddonModDataFieldDateComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule
],
providers: [
AddonModDataFieldDateHandler
],
exports: [
AddonModDataFieldDateComponent
],
entryComponents: [
AddonModDataFieldDateComponent
]
})
export class AddonModDataFieldDateModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldDateHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,180 @@
// (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 { AddonModDataFieldDateComponent } from '../component/date';
/**
* Handler for date data field plugin.
*/
@Injectable()
export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldDateHandler';
type = 'date';
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> {
return AddonModDataFieldDateComponent;
}
/**
* 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,
enabledName = 'f_' + field.id + '_z';
if (inputData[enabledName] && typeof inputData[fieldName] == 'string') {
const values = [],
date = inputData[fieldName].substr(0, 10).split('-'),
year = date[0],
month = date[1],
day = date[2];
values.push({
name: fieldName + '_y',
value: year
});
values.push({
name: fieldName + '_m',
value: month
});
values.push({
name: fieldName + '_d',
value: day
});
values.push({
name: enabledName,
value: 1
});
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;
if (typeof inputData[fieldName] == 'string') {
const values = [],
date = inputData[fieldName].substr(0, 10).split('-'),
year = date[0],
month = date[1],
day = date[2];
values.push({
fieldid: field.id,
subfield: 'year',
value: year
});
values.push({
fieldid: field.id,
subfield: 'month',
value: month
});
values.push({
fieldid: field.id,
subfield: 'day',
value: day
});
return values;
}
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,
input = inputData[fieldName] && inputData[fieldName].substr(0, 10) || '';
originalFieldData = (originalFieldData && originalFieldData.content &&
new Date(originalFieldData.content * 1000).toISOString().substr(0, 10)) || '';
return input != 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 < 2 || !inputData[0].value || !inputData[1].value || !inputData[2].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 {
let date = Date.UTC(offlineContent['year'] || '', offlineContent['month'] ? offlineContent['month'] - 1 : null,
offlineContent['day'] || null);
date = Math.floor(date / 1000);
originalContent.content = date || '';
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,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 { AddonModDataFieldCheckboxModule } from './checkbox/checkbox.module';
import { AddonModDataFieldDateModule } from './date/date.module';
import { AddonModDataFieldFileModule } from './file/file.module';
import { AddonModDataFieldLatlongModule } from './latlong/latlong.module';
import { AddonModDataFieldMenuModule } from './menu/menu.module';
import { AddonModDataFieldMultimenuModule } from './multimenu/multimenu.module';
import { AddonModDataFieldNumberModule } from './number/number.module';
import { AddonModDataFieldPictureModule } from './picture/picture.module';
import { AddonModDataFieldRadiobuttonModule } from './radiobutton/radiobutton.module';
import { AddonModDataFieldTextModule } from './text/text.module';
import { AddonModDataFieldTextareaModule } from './textarea/textarea.module';
import { AddonModDataFieldUrlModule } from './url/url.module';
@NgModule({
declarations: [],
imports: [
AddonModDataFieldCheckboxModule,
AddonModDataFieldDateModule,
AddonModDataFieldFileModule,
AddonModDataFieldLatlongModule,
AddonModDataFieldMenuModule,
AddonModDataFieldMultimenuModule,
AddonModDataFieldNumberModule,
AddonModDataFieldPictureModule,
AddonModDataFieldRadiobuttonModule,
AddonModDataFieldTextModule,
AddonModDataFieldTextareaModule,
AddonModDataFieldUrlModule
],
providers: [
],
exports: []
})
export class AddonModDataFieldModule { }

View File

@ -0,0 +1,19 @@
<span *ngIf="mode == 'edit'">
<span [core-mark-required]="field.required" class="core-mark-required"></span>
<core-attachments [files]="files" [maxSize]="maxSizeBytes" maxSubmissions="1" [component]="component" [componentId]="componentId" [allowOffline]="true"></core-attachments>
<core-input-errors *ngIf="error" [errorText]="error"></core-input-errors>
</span>
<span *ngIf="mode == 'search'" [formGroup]="form">
<ion-input type="text" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
</span>
<ng-container *ngIf="isShowOrListMode()">
<div *ngFor="let file of files" no-lines>
<!-- Files already attached to the submission. -->
<core-file *ngIf="!file.name" [file]="file" [component]="component" [componentId]="componentId" [alwaysDownload]="true"></core-file>
<!-- Files stored in offline to be sent later. -->
<core-local-file *ngIf="file.name" [file]="file"></core-local-file>
</div>
</ng-container>

View File

@ -0,0 +1,83 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
import { CoreFileSessionProvider } from '@providers/file-session';
import { AddonModDataProvider } from '../../../providers/data';
/**
* Component to render data file field.
*/
@Component({
selector: 'addon-mod-data-field-file',
templateUrl: 'file.html'
})
export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginComponent {
files = [];
component: string;
componentId: number;
maxSizeBytes: number;
constructor(protected fb: FormBuilder, private fileSessionprovider: CoreFileSessionProvider) {
super(fb);
}
/**
* Get the files from the input value.
*
* @param {any} value Input value.
* @return {any} List of files.
*/
protected getFiles(value: any): any {
let files = (value && value.files) || [];
// Reduce to first element.
if (files.length > 0) {
files = [files[0]];
}
return files;
}
/**
* Initialize field.
*/
protected init(): void {
if (this.mode != 'search') {
this.component = AddonModDataProvider.COMPONENT;
this.componentId = this.database.coursemodule;
this.updateValue(this.value);
if (this.mode == 'edit') {
this.maxSizeBytes = parseInt(this.field.param3, 10);
this.fileSessionprovider.setFiles(this.component, this.database.id + '_' + this.field.id, this.files);
}
} else {
this.addControl('f_' + this.field.id);
}
}
/**
* Update value being shown.
*
* @param {any} value New value to be set.
*/
protected updateValue(value: any): void {
this.value = value;
this.files = this.getFiles(value);
}
}

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 { AddonModDataFieldFileHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldFileComponent } from './component/file';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldFileComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
],
providers: [
AddonModDataFieldFileHandler
],
exports: [
AddonModDataFieldFileComponent
],
entryComponents: [
AddonModDataFieldFileComponent
]
})
export class AddonModDataFieldFileModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldFileHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,158 @@
// (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 { CoreFileSessionProvider } from '@providers/file-session';
import { AddonModDataFieldHandler } from '../../../providers/fields-delegate';
import { AddonModDataProvider } from '../../../providers/data';
import { AddonModDataFieldFileComponent } from '../component/file';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
/**
* Handler for file data field plugin.
*/
@Injectable()
export class AddonModDataFieldFileHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldFileHandler';
type = 'file';
constructor(private translate: TranslateService, private fileSessionprovider: CoreFileSessionProvider,
private fileUploaderProvider: CoreFileUploaderProvider) { }
/**
* 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> {
return AddonModDataFieldFileComponent;
}
/**
* 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;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName]
}];
}
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 files = this.getFieldEditFiles(field);
if (files.length) {
return [{
fieldid: field.id,
subfield: 'file',
files: files
}];
}
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): any {
return this.fileSessionprovider.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id);
}
/**
* 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 files = this.fileSessionprovider.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id) || [];
let originalFiles = (originalFieldData && originalFieldData.files) || [];
if (originalFiles.length) {
originalFiles = [originalFiles[0]];
}
return this.fileUploaderProvider.areFileListDifferent(files, originalFiles);
}
/**
* 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 {
if (offlineContent && offlineContent.file && offlineContent.file.offline > 0 && offlineFiles && offlineFiles.length > 0) {
originalContent.content = offlineFiles[0].filename;
originalContent.files = [offlineFiles[0]];
} else if (offlineContent && offlineContent.file && offlineContent.file.online && offlineContent.file.online.length > 0) {
originalContent.content = offlineContent.file.online[0].filename;
originalContent.files = [offlineContent.file.online[0]];
}
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,19 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<ion-input *ngIf="mode == 'search'" type="text" [placeholder]="field.name" [formControlName]="'f_'+field.id"></ion-input>
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<div class="addon-data-lantlong">
<ion-input type="text" [formControlName]="'f_'+field.id+'_0'" maxlength="10"></ion-input>
<span class="placeholder-icon" item-right>°N</span>
</div>
<div class="addon-data-lantlong">
<ion-input type="text" [formControlName]="'f_'+field.id+'_1'" maxlength="10"></ion-input>
<span class="placeholder-icon" item-right>°E</span>
</div>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<span *ngIf="isShowOrListMode() && value">
<a [href]="getLatLongLink(north, east)">{{ formatLatLong(north, east) }}</a>
</span>

View File

@ -0,0 +1,97 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Platform } from 'ionic-angular';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data latlong field.
*/
@Component({
selector: 'addon-mod-data-field-latlong',
templateUrl: 'latlong.html'
})
export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginComponent {
north: number;
east: number;
constructor(protected fb: FormBuilder, private platform: Platform) {
super(fb);
}
/**
* Format latitude and longitude in a simple text.
*
* @param {number} north Degrees north.
* @param {number} east Degrees East.
* @return {string} Readable Latitude and logitude.
*/
formatLatLong(north: number, east: number): string {
if (north !== null || east !== null) {
const northFixed = north ? Math.abs(north).toFixed(4) : '0.0000',
eastFixed = east ? Math.abs(east).toFixed(4) : '0.0000';
return northFixed + (north < 0 ? '°S' : '°N') + ' ' + eastFixed + (east < 0 ? '°W' : '°E');
}
}
/**
* Get link to maps from latitude and longitude.
*
* @param {number} north Degrees north.
* @param {number} east Degrees East.
* @return {string} Link to maps depending on platform.
*/
getLatLongLink(north: number, east: number): string {
if (north !== null || east !== null) {
const northFixed = north ? north.toFixed(4) : '0.0000',
eastFixed = east ? east.toFixed(4) : '0.0000';
if (this.platform.is('ios')) {
return 'http://maps.apple.com/?ll=' + northFixed + ',' + eastFixed + '&near=' + northFixed + ',' + eastFixed;
}
return 'geo:' + northFixed + ',' + eastFixed;
}
}
/**
* Initialize field.
*/
protected init(): void {
if (this.value) {
this.updateValue(this.value);
}
if (this.mode == 'edit') {
this.addControl('f_' + this.field.id + '_0', this.north);
this.addControl('f_' + this.field.id + '_1', this.east);
} else if (this.mode == 'search') {
this.addControl('f_' + this.field.id);
}
}
/**
* Update value being shown.
*
* @param {any} value New value to be set.
*/
protected updateValue(value: any): void {
this.value = value;
this.north = (value && parseFloat(value.content)) || null;
this.east = (value && parseFloat(value.content1)) || null;
}
}

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 { AddonModDataFieldLatlongHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldLatlongComponent } from './component/latlong';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldLatlongComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
AddonModDataFieldLatlongHandler
],
exports: [
AddonModDataFieldLatlongComponent
],
entryComponents: [
AddonModDataFieldLatlongComponent
]
})
export class AddonModDataFieldLatlongModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldLatlongHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,159 @@
// (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 { AddonModDataFieldLatlongComponent } from '../component/latlong';
/**
* Handler for latlong data field plugin.
*/
@Injectable()
export class AddonModDataFieldLatlongHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldLatlongHandler';
type = 'latlong';
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> {
return AddonModDataFieldLatlongComponent;
}
/**
* 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;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName]
}];
}
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,
values = [];
if (inputData[fieldName + '_0']) {
values.push({
fieldid: field.id,
subfield: '0',
value: inputData[fieldName + '_0']
});
}
if (inputData[fieldName + '_1']) {
values.push({
fieldid: field.id,
subfield: '1',
value: inputData[fieldName + '_1']
});
}
return values;
}
/**
* 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,
lat = inputData[fieldName + '_0'] || '',
long = inputData[fieldName + '_1'] || '',
originalLat = (originalFieldData && originalFieldData.content) || '',
originalLong = (originalFieldData && originalFieldData.content1) || '';
return lat != originalLat || long != originalLong;
}
/**
* 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 {
let valueCount = 0;
// The lat long class has two values that need to be checked.
inputData.forEach((value) => {
if (typeof value.value != 'undefined' && value.value != '') {
valueCount++;
}
});
// If we get here then only one field has been filled in.
if (valueCount == 1) {
return this.translate.instant('addon.mod_data.latlongboth');
} else if (field.required && valueCount == 0) {
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[0] || '';
originalContent.content1 = offlineContent[1] || '';
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,10 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-select [formControlName]="'f_'+field.id" [placeholder]="'addon.mod_data.menuchoose' | translate" [selectOptions]="{title: field.name}" interface="popover">
<ion-option value="">{{ 'addon.mod_data.menuchoose' | translate }}</ion-option>
<ion-option *ngFor="let option of options" [value]="option">{{option}}</ion-option>
</ion-select>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<core-format-text *ngIf="isShowOrListMode() && value && value.content" [text]="value.content"></core-format-text>

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 } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data menu field.
*/
@Component({
selector: 'addon-mod-data-field-menu',
templateUrl: 'menu.html'
})
export class AddonModDataFieldMenuComponent extends AddonModDataFieldPluginComponent {
options = [];
constructor(protected fb: FormBuilder) {
super(fb);
}
/**
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
return;
}
this.options = this.field.param1.split('\n');
let val;
if (this.mode == 'edit' && this.value) {
val = this.value.content;
}
this.addControl('f_' + this.field.id, val);
}
}

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 { AddonModDataFieldMenuHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldMenuComponent } from './component/menu';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldMenuComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
AddonModDataFieldMenuHandler
],
exports: [
AddonModDataFieldMenuComponent
],
entryComponents: [
AddonModDataFieldMenuComponent
]
})
export class AddonModDataFieldMenuModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldMenuHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,133 @@
// (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 { AddonModDataFieldMenuComponent } from '../component/menu';
/**
* Handler for menu data field plugin.
*/
@Injectable()
export class AddonModDataFieldMenuHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldMenuHandler';
type = 'menu';
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> {
return AddonModDataFieldMenuComponent;
}
/**
* 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;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName]
}];
}
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;
if (inputData[fieldName]) {
return [{
fieldid: field.id,
value: inputData[fieldName]
}];
}
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,
input = inputData[fieldName] || '';
originalFieldData = (originalFieldData && originalFieldData.content) || '';
return input != 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[''] || '';
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,16 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-select [formControlName]="'f_'+field.id" multiple="true" [placeholder]="'addon.mod_data.menuchoose' | translate" [selectOptions]="{title: field.name}" interface="popover">
<ion-option *ngFor="let option of options" [value]="option.value">{{option.key}}</ion-option>
</ion-select>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
<ion-item *ngIf="mode == 'search'">
<ion-label>{{ 'addon.mod_data.selectedrequired' | translate }}</ion-label>
<ion-checkbox item-end [formControlName]="'f_'+field.id+'_allreq'" [(ngModel)]="search['f_'+field.id+'_allreq']">
</ion-checkbox>
</ion-item>
</span>
<core-format-text *ngIf="isShowOrListMode() && value && value.content" [text]="value.content"></core-format-text>

View File

@ -0,0 +1,73 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data multimenu field.
*/
@Component({
selector: 'addon-mod-data-field-multimenu',
templateUrl: 'multimenu.html'
})
export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPluginComponent {
options = [];
constructor(protected fb: FormBuilder) {
super(fb);
}
/**
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
this.updateValue(this.value);
return;
}
this.options = this.field.param1.split('\n').map((option) => {
return { key: option, value: option };
});
const values = [];
if (this.mode == 'edit' && this.value && this.value.content) {
this.value.content.split('##').forEach((value) => {
const x = this.options.findIndex((option) => value == option.key);
if (x >= 0) {
values.push(value);
}
});
}
if (this.mode == 'search') {
this.addControl('f_' + this.field.id + '_allreq');
}
this.addControl('f_' + this.field.id, values);
}
/**
* Update value being shown.
*
* @param {any} value New value to be set.
*/
protected updateValue(value: any): void {
this.value = value;
this.value.content = value && value.content && value.content.split('##').join('<br>');
}
}

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 { AddonModDataFieldMultimenuHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldMultimenuComponent } from './component/multimenu';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldMultimenuComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
AddonModDataFieldMultimenuHandler
],
exports: [
AddonModDataFieldMultimenuComponent
],
entryComponents: [
AddonModDataFieldMultimenuComponent
]
})
export class AddonModDataFieldMultimenuModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldMultimenuHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,146 @@
// (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 { AddonModDataFieldMultimenuComponent } from '../component/multimenu';
/**
* Handler for multimenu data field plugin.
*/
@Injectable()
export class AddonModDataFieldMultimenuHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldMultimenuHandler';
type = 'multimenu';
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> {
return AddonModDataFieldMultimenuComponent;
}
/**
* 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';
if (inputData[fieldName] && inputData[fieldName].length > 0) {
const values = [];
values.push({
name: fieldName,
value: inputData[fieldName]
});
if (inputData[reqName]) {
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;
if (inputData[fieldName] && inputData[fieldName].length > 0) {
return [{
fieldid: field.id,
value: inputData[fieldName]
}];
}
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;
originalFieldData = (originalFieldData && originalFieldData.content) || '';
return inputData[fieldName].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,7 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-input type="number" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<core-format-text *ngIf="isShowOrListMode() && value && value.content" [text]="value.content"></core-format-text>

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 { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data number field.
*/
@Component({
selector: 'addon-mod-data-field-number',
templateUrl: 'number.html'
})
export class AddonModDataFieldNumberComponent extends AddonModDataFieldPluginComponent{
constructor(protected fb: FormBuilder) {
super(fb);
}
/**
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
return;
}
let value;
if (this.mode == 'edit' && this.value) {
const v = parseFloat(this.value.content);
value = isNaN(v) ? '' : v;
}
this.addControl('f_' + this.field.id, value);
}
}

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 { AddonModDataFieldNumberHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldNumberComponent } from './component/number';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldNumberComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
AddonModDataFieldNumberHandler
],
exports: [
AddonModDataFieldNumberComponent
],
entryComponents: [
AddonModDataFieldNumberComponent
]
})
export class AddonModDataFieldNumberModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldNumberHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,75 @@
// (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 { AddonModDataFieldTextHandler } from '../../text/providers/handler';
import { AddonModDataFieldNumberComponent } from '../component/number';
/**
* Handler for number data field plugin.
*/
@Injectable()
export class AddonModDataFieldNumberHandler extends AddonModDataFieldTextHandler {
name = 'AddonModDataFieldNumberHandler';
type = 'number';
constructor(protected translate: TranslateService) {
super(translate);
}
/**
* 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> {
return AddonModDataFieldNumberComponent;
}
/**
* 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,
input = typeof inputData[fieldName] != 'undefined' ? parseFloat(inputData[fieldName]) : '';
originalFieldData = (originalFieldData && typeof originalFieldData.content != 'undefined') ?
parseFloat(originalFieldData.content) : '';
return input != 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;
}
}

View File

@ -0,0 +1,16 @@
<span *ngIf="mode == 'edit'" [formGroup]="form">
<span [core-mark-required]="field.required" class="core-mark-required"></span>
<core-attachments [files]="files" [maxSize]="maxSizeBytes" maxSubmissions="1" [component]="component" [componentId]="componentId" [allowOffline]="true" acceptedTypes="image"></core-attachments>
<core-input-errors *ngIf="error" [errorText]="error"></core-input-errors>
<ion-label stacked>{{ 'addon.mod_data.alttext' | translate }}</ion-label>
<ion-input type="text" [formControlName]="'f_'+field.id+'_alttext'" [placeholder]=" 'addon.mod_data.alttext' | translate" ></ion-input>
</span>
<span *ngIf="mode == 'search'" [formGroup]="form">
<ion-input type="text" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
</span>
<span *ngIf="mode == 'list' && imageUrl" (click)="viewAction()"><img [src]="imageUrl" [alt]="title" [title]="title" class="core-media-adapt-width list_picture" core-external-content/></span>
<img *ngIf="mode == 'show' && imageUrl" [src]="imageUrl" [alt]="title" [title]="title" class="core-media-adapt-width list_picture" [width]="width" [height]="height" core-external-content/>

View File

@ -0,0 +1,136 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
import { CoreFileSessionProvider } from '@providers/file-session';
import { AddonModDataProvider } from '../../../providers/data';
/**
* Component to render data picture field.
*/
@Component({
selector: 'addon-mod-data-field-picture',
templateUrl: 'picture.html'
})
export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginComponent {
files = [];
component: string;
componentId: number;
maxSizeBytes: number;
image: any;
entryId: number;
imageUrl: string;
title: string;
width: string;
height: string;
constructor(protected fb: FormBuilder, private fileSessionprovider: CoreFileSessionProvider) {
super(fb);
}
/**
* Get the files from the input value.
*
* @param {any} value Input value.
* @return {any} List of files.
*/
protected getFiles(value: any): any {
let files = (value && value.files) || [];
// Reduce to first element.
if (files.length > 0) {
files = [files[0]];
}
return files;
}
/**
* Find file in a list.
*
* @param {any[]} files File list where to search.
* @param {string} filenameSeek Filename to search.
* @return {any} File found or false.
*/
protected findFile(files: any[], filenameSeek: string): any {
return files.find((file) => file.filename == filenameSeek) || false;
}
/**
* Initialize field.
*/
protected init(): void {
if (this.mode != 'search') {
this.component = AddonModDataProvider.COMPONENT;
this.componentId = this.database.coursemodule;
this.updateValue(this.value);
if (this.mode == 'edit') {
this.maxSizeBytes = parseInt(this.field.param3, 10);
this.fileSessionprovider.setFiles(this.component, this.database.id + '_' + this.field.id, this.files);
const alttext = (this.value && this.value.content1) || '';
this.addControl('f_' + this.field.id + '_alttext', alttext);
}
} else {
this.addControl('f_' + this.field.id);
}
}
/**
* Update value being shown.
*
* @param {any} value New value to be set.
*/
protected updateValue(value: any): void {
this.value = value;
// Edit mode, the list shouldn't change so there is no need to watch it.
const files = value && value.files || [];
// Get image or thumb.
if (files.length > 0) {
const filenameSeek = this.mode == 'list' ? 'thumb_' + value.content : value.content;
this.image = this.findFile(files, filenameSeek);
if (!this.image && this.mode == 'list') {
this.image = this.findFile(files, value.content);
}
this.files = [this.image];
} else {
this.image = false;
this.files = [];
}
if (this.mode != 'edit') {
this.entryId = (value && value.recordid) || null;
this.title = (value && value.content1) || '';
this.imageUrl = null;
if (this.image) {
if (this.image.offline) {
this.imageUrl = (this.image && this.image.toURL()) || null;
} else {
this.imageUrl = (this.image && this.image.fileurl) || null;
}
}
this.width = this.field.param1 || '';
this.height = this.field.param2 || '';
}
}
}

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 { AddonModDataFieldPictureHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldPictureComponent } from './component/picture';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldPictureComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
],
providers: [
AddonModDataFieldPictureHandler
],
exports: [
AddonModDataFieldPictureComponent
],
entryComponents: [
AddonModDataFieldPictureComponent
]
})
export class AddonModDataFieldPictureModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldPictureHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,194 @@
// (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 { CoreFileSessionProvider } from '@providers/file-session';
import { AddonModDataFieldHandler } from '../../../providers/fields-delegate';
import { AddonModDataProvider } from '../../../providers/data';
import { AddonModDataFieldPictureComponent } from '../component/picture';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
/**
* Handler for picture data field plugin.
*/
@Injectable()
export class AddonModDataFieldPictureHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldPictureHandler';
type = 'picture';
constructor(private translate: TranslateService, private fileSessionprovider: CoreFileSessionProvider,
private fileUploaderProvider: CoreFileUploaderProvider) { }
/**
* 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> {
return AddonModDataFieldPictureComponent;
}
/**
* 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;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName]
}];
}
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 files = this.getFieldEditFiles(field),
values = [],
fieldName = 'f_' + field.id + '_alttext';
if (files.length) {
values.push({
fieldid: field.id,
subfield: 'file',
files: files
});
}
if (inputData[fieldName]) {
values.push({
fieldid: field.id,
subfield: 'alttext',
value: inputData[fieldName]
});
}
return values;
}
/**
* 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): any {
return this.fileSessionprovider.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id);
}
/**
* 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 + '_alttext',
altText = inputData[fieldName] || '',
originalAltText = (originalFieldData && originalFieldData.content1) || '',
files = this.getFieldEditFiles(field) || [];
let originalFiles = (originalFieldData && originalFieldData.files) || [];
// Get image.
if (originalFiles.length > 0) {
const filenameSeek = (originalFieldData && originalFieldData.content) || '',
file = originalFiles.find((file) => file.filename == filenameSeek);
if (file) {
originalFiles = [file];
}
} else {
originalFiles = [];
}
return altText != originalAltText || this.fileUploaderProvider.areFileListDifferent(files, originalFiles);
}
/**
* 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) {
if (!inputData || !inputData.length) {
return this.translate.instant('addon.mod_data.errormustsupplyvalue');
}
const found = inputData.some((input) => {
if (typeof input.subfield != 'undefined' && input.subfield == 'file') {
return !!input.value;
}
return false;
});
if (!found) {
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 {
if (offlineContent && offlineContent.file && offlineContent.file.offline > 0 && offlineFiles && offlineFiles.length > 0) {
originalContent.content = offlineFiles[0].filename;
originalContent.files = [offlineFiles[0]];
} else if (offlineContent && offlineContent.file && offlineContent.file.online && offlineContent.file.online.length > 0) {
originalContent.content = offlineContent.file.online[0].filename;
originalContent.files = [offlineContent.file.online[0]];
}
originalContent.content1 = offlineContent.alttext || '';
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,10 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-select [formControlName]="'f_'+field.id" [placeholder]="'addon.mod_data.menuchoose' | translate" [selectOptions]="{title: field.name}" interface="popover">
<ion-option value="">{{ 'addon.mod_data.menuchoose' | translate }}</ion-option>
<ion-option *ngFor="let option of options" [value]="option">{{option}}</ion-option>
</ion-select>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<core-format-text *ngIf="isShowOrListMode() && value && value.content" [text]="value.content"></core-format-text>

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 } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data radiobutton field.
*/
@Component({
selector: 'addon-mod-data-field-radiobutton',
templateUrl: 'radiobutton.html'
})
export class AddonModDataFieldRadiobuttonComponent extends AddonModDataFieldPluginComponent {
options = [];
constructor(protected fb: FormBuilder) {
super(fb);
}
/**
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
return;
}
this.options = this.field.param1.split('\n');
let val;
if (this.mode == 'edit' && this.value) {
val = this.value.content;
}
this.addControl('f_' + this.field.id, val);
}
}

View File

@ -0,0 +1,133 @@
// (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 { AddonModDataFieldRadiobuttonComponent } from '../component/radiobutton';
/**
* Handler for checkbox data field plugin.
*/
@Injectable()
export class AddonModDataFieldRadiobuttonHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldRadiobuttonHandler';
type = 'radiobutton';
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> {
return AddonModDataFieldRadiobuttonComponent;
}
/**
* 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;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName]
}];
}
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;
if (inputData[fieldName]) {
return [{
fieldid: field.id,
value: inputData[fieldName]
}];
}
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,
input = inputData[fieldName] || '';
originalFieldData = (originalFieldData && originalFieldData.content) || '';
return input != 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[''] || '';
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,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 { AddonModDataFieldRadiobuttonHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldRadiobuttonComponent } from './component/radiobutton';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldRadiobuttonComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
AddonModDataFieldRadiobuttonHandler
],
exports: [
AddonModDataFieldRadiobuttonComponent
],
entryComponents: [
AddonModDataFieldRadiobuttonComponent
]
})
export class AddonModDataFieldRadiobuttonModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldRadiobuttonHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,7 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-input type="text" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<core-format-text *ngIf="isShowOrListMode() && value && value.content" [text]="value.content"></core-format-text>

View File

@ -0,0 +1,46 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data text field.
*/
@Component({
selector: 'addon-mod-data-field-text',
templateUrl: 'text.html'
})
export class AddonModDataFieldTextComponent extends AddonModDataFieldPluginComponent {
constructor(protected fb: FormBuilder) {
super(fb);
}
/**
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
return;
}
let value;
if (this.mode == 'edit' && this.value) {
value = this.value.content;
}
this.addControl('f_' + this.field.id, value);
}
}

View File

@ -0,0 +1,134 @@
// (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 { AddonModDataFieldTextComponent } from '../component/text';
/**
* Handler for number data field plugin.
*/
@Injectable()
export class AddonModDataFieldTextHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldTextHandler';
type = 'text';
constructor(protected 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> {
return AddonModDataFieldTextComponent;
}
/**
* 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;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName]
}];
}
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;
if (inputData[fieldName]) {
return [{
fieldid: field.id,
value: inputData[fieldName]
}];
}
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,
input = inputData[fieldName] || '';
originalFieldData = (originalFieldData && originalFieldData.content) || '';
return input != 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[''] || '';
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,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 { AddonModDataFieldTextHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldTextComponent } from './component/text';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldTextComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
AddonModDataFieldTextHandler
],
exports: [
AddonModDataFieldTextComponent
],
entryComponents: [
AddonModDataFieldTextComponent
]
})
export class AddonModDataFieldTextModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldTextHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,9 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<ion-input *ngIf="mode == 'search'" type="text" [placeholder]="field.name" [formControlName]="'f_'+field.id"></ion-input>
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<core-rich-text-editor *ngIf="mode == 'edit'" item-content [control]="form.controls['f_'+field.id]" [placeholder]="field.name" [formControlName]="'f_'+field.id" [component]="component" [componentId]="componentId"></core-rich-text-editor>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<core-format-text *ngIf="isShowOrListMode() && value" [text]="format(value)" [component]="component" [componentId]="componentId"></core-format-text>

View File

@ -0,0 +1,68 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { AddonModDataProvider } from '../../../providers/data';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data number field.
*/
@Component({
selector: 'addon-mod-data-field-textarea',
templateUrl: 'textarea.html'
})
export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginComponent {
component: string;
componentId: number;
constructor(protected fb: FormBuilder, protected textUtils: CoreTextUtilsProvider) {
super(fb);
}
/**
* Format value to be shown. Replacing plugin file Urls.
*
* @param {any} value Value to replace.
* @return {string} Replaced string to be rendered.
*/
format(value: any): string {
const files = (value && value.files) || [];
return value ? this.textUtils.replacePluginfileUrls(value.content, files) : '';
}
/**
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
this.component = AddonModDataProvider.COMPONENT;
this.componentId = this.database.coursemodule;
return;
}
let text;
// Check if rich text editor is enabled.
if (this.mode == 'edit') {
const files = (this.value && this.value.files) || [];
text = this.value ? this.textUtils.replacePluginfileUrls(this.value.content, files) : '';
}
this.addControl('f_' + this.field.id, text);
}
}

View File

@ -0,0 +1,145 @@
// (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 { AddonModDataFieldTextHandler } from '../../text/providers/handler';
import { AddonModDataFieldTextareaComponent } from '../component/textarea';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
/**
* Handler for textarea data field plugin.
*/
@Injectable()
export class AddonModDataFieldTextareaHandler extends AddonModDataFieldTextHandler {
name = 'AddonModDataFieldTextareaHandler';
type = 'textarea';
constructor(protected translate: TranslateService, private textUtils: CoreTextUtilsProvider,
private domUtils: CoreDomUtilsProvider) {
super(translate);
}
/**
* 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> {
return AddonModDataFieldTextareaComponent;
}
/**
* 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;
if (inputData[fieldName]) {
return this.domUtils.isRichTextEditorEnabled().then((enabled) => {
const files = this.getFieldEditFiles(field, inputData, originalFieldData);
let text = this.textUtils.restorePluginfileUrls(inputData[fieldName], files);
if (!enabled) {
// Rich text editor not enabled, add some HTML to the text if needed.
text = this.textUtils.formatHtmlLines(text);
}
return [{
fieldid: field.id,
value: text
},
{
fieldid: field.id,
subfield: 'content1',
value: 1
},
{
fieldid: field.id,
subfield: 'itemid',
files: files
}
];
});
}
return false;
}
/**
* Get field edit files in the input data.
*
* @param {any} field Defines the field..
* @param {any} inputData Data entered in the edit form.
* @param {any} originalFieldData Original field entered data.
* @return {any} With name and value of the data to be sent.
*/
getFieldEditFiles(field: any, inputData: any, originalFieldData: any): any {
return (originalFieldData && originalFieldData.files) || [];
}
/**
* 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) {
if (!inputData || !inputData.length) {
return this.translate.instant('addon.mod_data.errormustsupplyvalue');
}
const found = inputData.some((input) => {
if (!input.subfield) {
return !!input.value;
}
return false;
});
if (!found) {
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[''] || '';
if (originalContent.content.length > 0 && originalContent.files && originalContent.files.length > 0) {
// Take the original files since we cannot edit them on the app.
originalContent.content = this.textUtils.replacePluginfileUrls(originalContent.content, originalContent.files);
}
return originalContent;
}
}

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 { AddonModDataFieldTextareaHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldTextareaComponent } from './component/textarea';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldTextareaComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
AddonModDataFieldTextareaHandler
],
exports: [
AddonModDataFieldTextareaComponent
],
entryComponents: [
AddonModDataFieldTextareaComponent
]
})
export class AddonModDataFieldTextareaModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldTextareaHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,7 @@
<span *ngIf="!isShowOrListMode()" [formGroup]="form">
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-input type="url" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<a *ngIf="isShowOrListMode() && value && value.content" [href]="value.content" core-link capture="true">{{field.name}}</a>

View File

@ -0,0 +1,46 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data url field.
*/
@Component({
selector: 'addon-mod-data-field-url',
templateUrl: 'url.html'
})
export class AddonModDataFieldUrlComponent extends AddonModDataFieldPluginComponent {
constructor(protected fb: FormBuilder) {
super(fb);
}
/**
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
return;
}
let value;
if (this.mode == 'edit' && this.value) {
value = this.value.content;
}
this.addControl('f_' + this.field.id, value);
}
}

View File

@ -0,0 +1,57 @@
// (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 { AddonModDataFieldTextHandler } from '../../text/providers/handler';
import { AddonModDataFieldUrlComponent } from '../component/url';
/**
* Handler for url data field plugin.
*/
@Injectable()
export class AddonModDataFieldUrlHandler extends AddonModDataFieldTextHandler {
name = 'AddonModDataFieldUrlHandler';
type = 'url';
constructor(protected translate: TranslateService) {
super(translate);
}
/**
* 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> {
return AddonModDataFieldUrlComponent;
}
/**
* 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;
}
}

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 { AddonModDataFieldUrlHandler } from './providers/handler';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldUrlComponent } from './component/url';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonModDataFieldUrlComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
AddonModDataFieldUrlHandler
],
exports: [
AddonModDataFieldUrlComponent
],
entryComponents: [
AddonModDataFieldUrlComponent
]
})
export class AddonModDataFieldUrlModule {
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldUrlHandler) {
fieldDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,41 @@
{
"addentries": "Add entries",
"advancedsearch": "Advanced search",
"alttext": "Alternative text",
"approve": "Approve",
"approved": "Approved",
"ascending": "Ascending",
"authorfirstname": "Author first name",
"authorlastname": "Author surname",
"confirmdeleterecord": "Are you sure you want to delete this entry?",
"descending": "Descending",
"disapprove": "Undo approval",
"emptyaddform": "You did not fill out any fields!",
"entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity",
"entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.",
"errorapproving": "Error approving or unapproving entry.",
"errordeleting": "Error deleting entry.",
"errormustsupplyvalue": "You must supply a value here.",
"expired": "Sorry, this activity closed on {{$a}} and is no longer available",
"fields": "Fields",
"latlongboth": "Both latitude and longitude are required.",
"menuchoose": "Choose...",
"more": "More",
"nomatch": "No matching entries found!",
"norecords": "No entries in database",
"notapproved": "Entry is not approved yet.",
"notopenyet": "Sorry, this activity is not available until {{$a}}",
"numrecords": "{{$a}} entries",
"other": "Other",
"recordapproved": "Entry approved",
"recorddeleted": "Entry deleted",
"recorddisapproved": "Entry unapproved",
"resetsettings": "Reset filters",
"search": "Search",
"single": "View single",
"selectedrequired": "All selected required",
"single": "View single",
"timeadded": "Time added",
"timemodified": "Time modified",
"usedate": "Include in search."
}

View File

@ -0,0 +1,31 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="save()" [attr.aria-label]="'core.save' | translate">
<ion-icon name="send"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<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="addon-data-contents {{cssClass}}">
<style *ngIf="cssTemplate">
{{ cssTemplate }}
</style>
<form (ngSubmit)="save()" [formGroup]="editForm">
<core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
</form>
</div>
</core-loading>
</ion-content>

View File

@ -0,0 +1,39 @@
// (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 { CoreComponentsModule } from '@components/components.module';
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module';
import { AddonModDataComponentsModule } from '../../components/components.module';
import { AddonModDataEditPage } from './edit';
@NgModule({
declarations: [
AddonModDataEditPage,
],
imports: [
CoreDirectivesModule,
CoreComponentsModule,
AddonModDataComponentsModule,
CoreCompileHtmlComponentModule,
CoreCommentsComponentsModule,
IonicPageModule.forChild(AddonModDataEditPage),
TranslateModule.forChild()
],
})
export class AddonModDataEditPageModule {}

View File

@ -0,0 +1,363 @@
// (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 { Content, IonicPage, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { FormGroup } from '@angular/forms';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSitesProvider } from '@providers/sites';
import { CoreGroupsProvider } from '@providers/groups';
import { CoreEventsProvider } from '@providers/events';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { CoreCourseProvider } from '@core/course/providers/course';
import { AddonModDataProvider } from '../../providers/data';
import { AddonModDataHelperProvider } from '../../providers/helper';
import { AddonModDataOfflineProvider } from '../../providers/offline';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataComponentsModule } from '../../components/components.module';
/**
* Page that displays the view edit page.
*/
@IonicPage({ segment: 'addon-mod-data-edit' })
@Component({
selector: 'page-addon-mod-data-edit',
templateUrl: 'edit.html',
})
export class AddonModDataEditPage {
@ViewChild(Content) content: Content;
protected module: any;
protected courseId: number;
protected data: any;
protected entryId: number;
protected entry: any;
protected offlineActions = [];
protected fields = {};
protected fieldsArray = [];
protected siteId: string;
protected offline: boolean;
protected forceLeave = false; // To allow leaving the page without checking for changes.
title = '';
component = AddonModDataProvider.COMPONENT;
loaded = false;
selectedGroup = 0;
cssClass = '';
cssTemplate = '';
groupInfo: any;
editFormRender = '';
editForm: FormGroup;
extraImports = [AddonModDataComponentsModule];
jsData: any;
errors = {};
constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate,
protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider,
protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider,
sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected translate: TranslateService,
protected eventsProvider: CoreEventsProvider, protected fileUploaderProvider: CoreFileUploaderProvider) {
this.module = params.get('module') || {};
this.entryId = params.get('entryId') || null;
this.courseId = params.get('courseId');
this.selectedGroup = params.get('group') || 0;
this.siteId = sitesProvider.getCurrentSiteId();
this.title = this.module.name;
this.editForm = new FormGroup({});
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchEntryData();
}
/**
* Check if we can leave the page or not and ask to confirm the lost of data.
*
* @return {boolean | Promise<void>} Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
if (this.forceLeave) {
return true;
}
const inputData = this.editForm.value;
return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id,
this.entry.contents).then((changed) => {
if (!changed) {
return Promise.resolve();
}
// Show confirmation if some data has been modified.
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}).then(() => {
// Delete the local files from the tmp folder.
return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id,
this.entry.contents).then((files) => {
this.fileUploaderProvider.clearTmpFiles(files);
});
});
}
/**
* Fetch the entry data.
*
* @return {Promise<any>} Resolved when done.
*/
protected fetchEntryData(): Promise<any> {
return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => {
this.title = data.name || this.title;
this.data = data;
this.cssClass = 'addon-data-entries-' + data.id;
return this.dataProvider.getDatabaseAccessInformation(data.id);
}).then((accessData) => {
this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.' + this.cssClass);
if (this.entryId) {
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;
}
}
});
}
}).then(() => {
return this.dataOffline.getEntryActions(this.data.id, this.entryId);
}).then((actions) => {
this.offlineActions = actions;
return this.dataProvider.getFields(this.data.id);
}).then((fieldsData) => {
this.fieldsArray = fieldsData;
this.fields = this.utils.arrayToObject(fieldsData, 'id');
return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions);
}).then((entry) => {
if (entry) {
entry = entry.entry;
// Index contents by fieldid.
entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid');
} else {
entry = {
contents: {}
};
}
return this.dataHelper.applyOfflineActions(entry, this.offlineActions, this.fieldsArray);
}).then((entryData) => {
this.entry = entryData;
this.editFormRender = this.displayEditFields();
}).catch((message) => {
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
}).finally(() => {
this.loaded = true;
});
}
/**
* Saves data.
*
* @return {Promise<any>} Resolved when done.
*/
save(): Promise<any> {
const inputData = this.editForm.value;
return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id,
this.entry.contents).then((changed) => {
if (!changed) {
if (this.entryId) {
return this.returnToEntryList();
}
// New entry, no changes means no field filled, warn the user.
return Promise.reject('addon.mod_data.emptyaddform');
}
const modal = this.domUtils.showModalLoading('core.sending', true);
// Create an ID to assign files.
const entryTemp = this.entryId ? this.entryId : - (new Date().getTime());
return this.dataHelper.getEditDataFromForm(inputData, this.fieldsArray, this.data.id, entryTemp, this.entry.contents,
this.offline).catch((e) => {
if (!this.offline) {
// Cannot submit in online, prepare for offline usage.
this.offline = true;
return this.dataHelper.getEditDataFromForm(inputData, this.fieldsArray, this.data.id, entryTemp,
this.entry.contents, this.offline);
}
return Promise.reject(e);
}).then((editData) => {
if (editData.length > 0) {
if (this.entryId) {
return this.dataProvider.editEntry(this.data.id, this.entryId, this.courseId, editData, this.fields,
undefined, this.offline);
}
return this.dataProvider.addEntry(this.data.id, entryTemp, this.courseId, editData, this.selectedGroup,
this.fields, undefined, this.offline);
}
return false;
}).then((result: any) => {
if (!result) {
// No field filled, warn the user.
return Promise.reject('addon.mod_data.emptyaddform');
}
// This is done if entry is updated when editing or creating if not.
if ((this.entryId && result.updated) || (!this.entryId && result.newentryid)) {
const promises = [];
this.entryId = this.entryId || result.newentryid;
promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId));
promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId));
return Promise.all(promises).then(() => {
this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED,
{ dataId: this.data.id, entryId: this.entryId } , this.siteId);
}).finally(() => {
return this.returnToEntryList();
});
} else {
this.errors = {};
result.fieldnotifications.forEach((fieldNotif) => {
const field = this.fieldsArray.find((field) => field.name == fieldNotif.fieldname);
if (field) {
this.errors[field.id] = fieldNotif.notification;
}
});
this.jsData['errors'] = this.errors;
setTimeout(() => {
this.scrollToFirstError();
});
}
}).finally(() => {
modal.dismiss();
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Cannot edit entry', true);
return Promise.reject(null);
});
}
/**
* Set group to see the database.
*
* @param {number} groupId Group identifier to set.
* @return {Promise<any>} Resolved when done.
*/
setGroup(groupId: number): Promise<any> {
this.selectedGroup = groupId;
this.loaded = false;
return this.fetchEntryData();
}
/**
* Displays Edit Search Fields.
*
* @return {string} Generated HTML.
*/
protected displayEditFields(): string {
if (!this.data.addtemplate) {
return '';
}
this.jsData = {
fields: this.fields,
contents: this.entry.contents,
form: this.editForm,
data: this.data,
errors: this.errors
};
let replace,
render,
template = this.data.addtemplate;
// Replace the fields found on template.
this.fieldsArray.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 mode="edit" [field]="fields[' + field.id + ']"\
[value]="contents[' + field.id + ']" [form]="form" [database]="data" [error]="errors[' + field.id + ']">\
</addon-mod-data-field-plugin>';
template = template.replace(replace, render);
// Replace the field id tag.
replace = '[[' + field.name + '#id]]';
replace = replace.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
replace = new RegExp(replace, 'gi');
template = template.replace(replace, 'field_' + field.id);
});
return template;
}
/**
* Return to the entry list (previous page) discarding temp data.
*
* @return {Promise<any>} Resolved when done.
*/
protected returnToEntryList(): Promise<any> {
const inputData = this.editForm.value;
return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id,
this.entry.contents).then((files) => {
this.fileUploaderProvider.clearTmpFiles(files);
}).finally(() => {
// Go back to entry list.
this.forceLeave = true;
this.navCtrl.pop();
});
}
/**
* Scroll to first error or to the top if not found.
*/
protected scrollToFirstError(): void {
if (!this.domUtils.scrollToElementBySelector(this.content, '.addon-data-error')) {
this.content.scrollToTop();
}
}
}

View File

@ -0,0 +1,54 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="entryLoaded" (ionRefresh)="refreshDatabase($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="entryLoaded">
<!-- Database entries found to be 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="addon-data-contents {{cssClass}}">
<style *ngIf="cssTemplate">
{{ cssTemplate }}
</style>
<core-compile-html [text]="entryRendered" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
</div>
<ion-item *ngIf="data && entry">
<core-comments contextLevel="module" [instanceId]="data.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry"></core-comments>
</ion-item>
<ion-grid *ngIf="previousId || nextId">
<ion-row align-items-center>
<ion-col *ngIf="previousId">
<button ion-button block outline icon-start (click)="gotoEntry(previousId)">
<ion-icon name="arrow-back"></ion-icon>
{{ 'core.previous' | translate }}
</button>
</ion-col>
<ion-col *ngIf="nextId">
<button ion-button block icon-end (click)="gotoEntry(nextId)">
{{ 'core.next' | translate }}
<ion-icon name="arrow-forward"></ion-icon>
</button>
</ion-col>
</ion-row>
</ion-grid>
</core-loading>
</ion-content>

View File

@ -0,0 +1,39 @@
// (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 { CoreComponentsModule } from '@components/components.module';
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module';
import { AddonModDataComponentsModule } from '../../components/components.module';
import { AddonModDataEntryPage } from './entry';
@NgModule({
declarations: [
AddonModDataEntryPage,
],
imports: [
CoreDirectivesModule,
CoreComponentsModule,
AddonModDataComponentsModule,
CoreCompileHtmlComponentModule,
CoreCommentsComponentsModule,
IonicPageModule.forChild(AddonModDataEntryPage),
TranslateModule.forChild()
],
})
export class AddonModDataEntryPageModule {}

View File

@ -0,0 +1,299 @@
// (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, OnDestroy } from '@angular/core';
import { Content, IonicPage, NavParams, NavController } from 'ionic-angular';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSitesProvider } from '@providers/sites';
import { CoreGroupsProvider } from '@providers/groups';
import { CoreEventsProvider } from '@providers/events';
import { CoreCourseProvider } from '@core/course/providers/course';
import { AddonModDataProvider } from '../../providers/data';
import { AddonModDataHelperProvider } from '../../providers/helper';
import { AddonModDataOfflineProvider } from '../../providers/offline';
import { AddonModDataSyncProvider } from '../../providers/sync';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataComponentsModule } from '../../components/components.module';
/**
* Page that displays the view entry page.
*/
@IonicPage({ segment: 'addon-mod-data-entry' })
@Component({
selector: 'page-addon-mod-data-entry',
templateUrl: 'entry.html',
})
export class AddonModDataEntryPage implements OnDestroy {
@ViewChild(Content) content: Content;
protected module: any;
protected entryId: number;
protected courseId: number;
protected page: number;
protected syncObserver: any; // It will observe the sync auto event.
protected entryChangedObserver: any; // It will observe the changed entry event.
protected fields = {};
title = '';
moduleName = 'data';
component = AddonModDataProvider.COMPONENT;
entryLoaded = false;
selectedGroup = 0;
entry: any;
offlineActions = [];
hasOffline = false;
cssTemplate = '';
previousId: number;
nextId: number;
access: any;
data: any;
groupInfo: any;
showComments: any;
entryRendered = '';
siteId: string;
cssClass = '';
extraImports = [AddonModDataComponentsModule];
jsData;
constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate,
protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider,
protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider,
sitesProvider: CoreSitesProvider, protected navCtrl: NavController,
protected eventsProvider: CoreEventsProvider) {
this.module = params.get('module') || {};
this.entryId = params.get('entryId') || null;
this.courseId = params.get('courseId');
this.selectedGroup = params.get('group') || 0;
this.page = params.get('page') || null;
this.siteId = sitesProvider.getCurrentSiteId();
this.title = this.module.name;
this.moduleName = this.courseProvider.translateModuleName('data');
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchEntryData();
// Refresh data if this discussion is synchronized automatically.
this.syncObserver = this.eventsProvider.on(AddonModDataSyncProvider.AUTO_SYNCED, (data) => {
if ((data.entryId == this.entryId || data.offlineEntryId == this.entryId) && this.data.id == data.dataId) {
if (data.deleted) {
// If deleted, go back.
this.navCtrl.pop();
} else {
this.entryId = data.entryid;
this.entryLoaded = false;
this.fetchEntryData(true);
}
}
}, this.siteId);
// Refresh entry on change.
this.entryChangedObserver = this.eventsProvider.on(AddonModDataProvider.ENTRY_CHANGED, (data) => {
if (data.entryId == this.entryId && this.data.id == data.dataId) {
if (data.deleted) {
// If deleted, go back.
this.navCtrl.pop();
} else {
this.entryLoaded = false;
this.fetchEntryData(true);
}
}
}, this.siteId);
}
/**
* Fetch the entry data.
*
* @param {boolean} refresh If refresh the current data or not.
* @return {Promise<any>} Resolved when done.
*/
protected fetchEntryData(refresh?: boolean): Promise<any> {
let fieldsArray;
return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => {
this.title = data.name || this.title;
this.data = data;
this.cssClass = 'addon-data-entries-' + data.id;
return this.setEntryIdFromPage(data.id, this.page, this.selectedGroup).then(() => {
return this.dataProvider.getDatabaseAccessInformation(data.id);
});
}).then((accessData) => {
this.access = accessData;
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.dataOffline.getEntryActions(this.data.id, this.entryId);
});
}).then((actions) => {
this.offlineActions = actions;
this.hasOffline = !!actions.length;
return this.dataProvider.getFields(this.data.id).then((fieldsData) => {
this.fields = this.utils.arrayToObject(fieldsData, 'id');
return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions);
});
}).then((entry) => {
entry = entry.entry;
this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.' + this.cssClass);
// Index contents by fieldid.
entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid');
fieldsArray = this.utils.objectToArray(this.fields);
return this.dataHelper.applyOfflineActions(entry, this.offlineActions, fieldsArray);
}).then((entryData) => {
this.entry = entryData;
const actions = this.dataHelper.getActions(this.data, this.access, this.entry);
this.entryRendered = this.dataHelper.displayShowFields(this.data.singletemplate, fieldsArray,
this.entry, 'show', actions);
this.showComments = actions.comments;
const entries = {};
entries[this.entryId] = this.entry;
// Pass the input data to the component.
this.jsData = {
fields: this.fields,
entries: entries,
data: this.data
};
return this.dataHelper.getPageInfoByEntry(this.data.id, this.entryId, this.selectedGroup).then((result) => {
this.previousId = result.previousId;
this.nextId = result.nextId;
});
}).catch((message) => {
if (!refresh) {
// Some call failed, retry without using cache since it might be a new activity.
return this.refreshAllData();
}
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
}).finally(() => {
this.content && this.content.scrollToTop();
this.entryLoaded = true;
});
}
/**
* Go to selected entry without changing state.
*
* @param {number} entry Entry Id where to go.
* @return {Promise<any>} Resolved when done.
*/
gotoEntry(entry: number): Promise<any> {
this.entryId = entry;
this.page = null;
this.entryLoaded = false;
return this.fetchEntryData();
}
/**
* Refresh all the data.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected refreshAllData(): Promise<any> {
const promises = [];
promises.push(this.dataProvider.invalidateDatabaseData(this.courseId));
if (this.data) {
promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId));
promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule));
promises.push(this.dataProvider.invalidateEntriesData(this.data.id));
}
return Promise.all(promises).finally(() => {
return this.fetchEntryData(true);
});
}
/**
* Refresh the data.
*
* @param {any} [refresher] Refresher.
* @return {Promise<any>} Promise resolved when done.
*/
refreshDatabase(refresher?: any): Promise<any> {
if (this.entryLoaded) {
return this.refreshAllData().finally(() => {
refresher && refresher.complete();
});
}
}
/**
* Set group to see the database.
*
* @param {number} groupId Group identifier to set.
* @return {Promise<any>} Resolved when done.
*/
setGroup(groupId: number): Promise<any> {
this.selectedGroup = groupId;
this.entryLoaded = false;
return this.setEntryIdFromPage(this.data.id, 0, this.selectedGroup).then(() => {
return this.fetchEntryData();
});
}
/**
* Convenience function to translate page number to entry identifier.
*
* @param {number} dataId Data Id.
* @param {number} [pageNumber] Page number where to go
* @param {number} group Group Id to get the entry.
* @return {Promise<any>} Resolved when done.
*/
protected setEntryIdFromPage(dataId: number, pageNumber?: number, group?: number): Promise<any> {
if (typeof pageNumber == 'number') {
return this.dataHelper.getPageInfoByPage(dataId, pageNumber, group).then((result) => {
this.entryId = result.entryId;
this.page = null;
});
}
return Promise.resolve();
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.syncObserver && this.syncObserver.off();
this.entryChangedObserver && this.entryChangedObserver.off();
}
}

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,55 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'addon.mod_data.search' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<div class="fixed-content core-tabs-bar">
<a [attr.aria-selected]="!search.searchingAdvanced" (click)="toggleAdvanced()">{{ 'addon.mod_data.search' | translate}}</a>
<a [attr.aria-selected]="search.searchingAdvanced" (click)="toggleAdvanced()">{{ 'addon.mod_data.advancedsearch' | translate }}</a>
</div>
<form (ngSubmit)="searchEntries()" [formGroup]="searchForm">
<ion-list no-margin>
<ion-item [hidden]="search.searchingAdvanced">
<ion-input type="text" placeholder="{{ 'addon.mod_data.search' | translate}}" [(ngModel)]="search.text" name="text" formControlName="text"></ion-input>
</ion-item>
<ion-item text-wrap>
<ion-label stacked>{{ 'core.sortby' | translate }}</ion-label>
<ion-select interface="popover" name="sortBy" formControlName="sortBy">
<optgroup *ngIf="fieldsArray.length" label="{{ 'addon.mod_data.fields' | translate }}">
<ion-option *ngFor="let field of fieldsArray" [value]="field.id">{{field.name}}</ion-option>
</optgroup>
<optgroup label="{{ 'addon.mod_data.other' | translate }}">
<ion-option value="0">{{ 'addon.mod_data.timeadded' | translate }}</ion-option>
<ion-option value="-4">{{ 'addon.mod_data.timemodified' | translate }}</ion-option>
<ion-option value="-1">{{ 'addon.mod_data.authorfirstname' | translate }}</ion-option>
<ion-option value="-2">{{ 'addon.mod_data.authorlastname' | translate }}</ion-option>
<ion-option value="-3" *ngIf="data.approval">{{ 'addon.mod_data.approved' | translate }}</ion-option>
</optgroup>
</ion-select>
</ion-item>
<ion-list radio-group [(ngModel)]="search.sortDirection" name="sortDirection" formControlName="sortDirection">
<ion-item>
<ion-label>{{ 'addon.mod_data.ascending' | translate }}</ion-label>
<ion-radio value="ASC"></ion-radio>
</ion-item>
<ion-item>
<ion-label>{{ 'addon.mod_data.descending' | translate }}</ion-label>
<ion-radio value="DESC"></ion-radio>
</ion-item>
</ion-list>
<div padding [hidden]="!advancedSearch || !search.searchingAdvanced" class="addon-data-advanced-search">
<core-compile-html [text]="advancedSearch" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
</div>
</ion-list>
<button ion-button block margin type="submit" icon-start>
<ion-icon name="search"></ion-icon>
{{ 'addon.mod_data.search' | translate }}
</button>
</form>
</ion-content>

View File

@ -0,0 +1,35 @@
// (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 { AddonModDataSearchPage } from './search';
import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module';
@NgModule({
declarations: [
AddonModDataSearchPage,
],
imports: [
CoreDirectivesModule,
AddonModDataComponentsModule,
CoreCompileHtmlComponentModule,
IonicPageModule.forChild(AddonModDataSearchPage),
TranslateModule.forChild()
],
})
export class AddonModDataSearchPageModule {}

View File

@ -0,0 +1,191 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { IonicPage, NavParams, ViewController } from 'ionic-angular';
import { FormBuilder, FormGroup } from '@angular/forms';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { AddonModDataComponentsModule } from '../../components/components.module';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
/**
* Page that displays the search modal.
*/
@IonicPage({ segment: 'addon-mod-data-search' })
@Component({
selector: 'page-addon-mod-data-search',
templateUrl: 'search.html',
})
export class AddonModDataSearchPage {
search: any;
fields: any;
data: any;
advancedSearch: any;
extraImports = [AddonModDataComponentsModule];
searchForm: FormGroup;
jsData: any;
fieldsArray: any;
constructor(params: NavParams, private viewCtrl: ViewController, fb: FormBuilder, protected utils: CoreUtilsProvider,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate,
protected textUtils: CoreTextUtilsProvider) {
this.search = params.get('search');
this.fields = params.get('fields');
this.data = params.get('data');
const advanced = {};
this.search.advanced.forEach((field) => {
advanced[field.name] = field.value ? this.textUtils.parseJSON(field.value) : '';
});
this.search.advanced = advanced;
this.searchForm = fb.group({
text: [this.search.text],
sortBy: [this.search.sortBy || 0],
sortDirection: [this.search.sortDirection || 'DESC'],
firstname: [this.search.advanced['firstname'] || ''],
lastname: [this.search.advanced['lastname'] || '']
});
this.fieldsArray = this.utils.objectToArray(this.fields);
this.advancedSearch = this.renderAdvancedSearchFields();
}
/**
* Displays Advanced Search Fields.
*
* @return {string} Generated HTML.
*/
protected renderAdvancedSearchFields(): string {
if (!this.data.asearchtemplate) {
return '';
}
this.jsData = {
fields: this.fields,
form: this.searchForm,
search: this.search.advanced
};
let template = this.data.asearchtemplate,
replace, render;
// Replace the fields found on template.
this.fieldsArray.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 mode="search" [field]="fields[' + field.id +
']" [form]="form" [search]="search"></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('##firstname##', 'gi');
render = '<span [formGroup]="form"><ion-input type="text" name="firstname" \
[placeholder]="\'addon.mod_data.authorfirstname\' | translate" formControlName="firstname"></ion-input></span>';
template = template.replace(replace, render);
// Replace lastname field by the text input.
replace = new RegExp('##lastname##', 'gi');
render = '<span [formGroup]="form"><ion-input type="text" name="lastname" \
[placeholder]="\'addon.mod_data.authorlastname\' | translate" formControlName="lastname"></ion-input></span>';
template = template.replace(replace, render);
return template;
}
/**
* Retrieve the entered data in search in a form.
*
* @param {any} searchedData Array with the entered form values.
* @return {any[]} Array with the answers.
*/
getSearchDataFromForm(searchedData: any): any[] {
const advancedSearch = [];
// Filter and translate fields to each field plugin.
this.fieldsArray.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;
}
/**
* Close modal.
*
* @param {any} [data] Data to return to the page.
*/
closeModal(data?: any): void {
this.viewCtrl.dismiss(data);
}
/**
* Toggles between advanced to normal search.
*/
toggleAdvanced(): void {
this.search.searchingAdvanced = !this.search.searchingAdvanced;
}
/**
* Done editing.
*/
searchEntries(): void {
const searchedData = this.searchForm.value;
if (this.search.searchingAdvanced) {
this.search.advanced = this.getSearchDataFromForm(searchedData);
this.search.searching = this.search.advanced.length > 0;
} else {
this.search.text = searchedData.text;
this.search.searching = this.search.text.length > 0;
}
this.search.sortBy = searchedData.sortBy;
this.search.sortDirection = searchedData.sortDirection;
this.closeModal(this.search);
}
}

View File

@ -0,0 +1,119 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { AddonModDataProvider } from './data';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreEventsProvider } from '@providers/events';
/**
* Content links handler for database approve/disapprove entry.
* Match mod/data/view.php?d=6&approve=5 with a valid data id and entryid.
*/
@Injectable()
export class AddonModDataApproveLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonModDataApproveLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModData';
pattern = /\/mod\/data\/view\.php.*([\?\&](d|approve|disapprove)=\d+)/;
constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider,
private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider) {
super();
}
/**
* Convenience function to help get courseId.
*
* @param {number} dataId Database Id.
* @param {string} siteId Site Id, if not set, current site will be used.
* @param {number} courseId Course Id if already set.
* @return {Promise<number>} Resolved with course Id when done.
*/
protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise<number> {
if (courseId) {
return Promise.resolve(courseId);
}
return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => {
return module.course;
});
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?): void => {
const modal = this.domUtils.showModalLoading(),
dataId = parseInt(params.d, 10),
entryId = parseInt(params.approve, 10) || parseInt(params.disapprove, 10),
approve = parseInt(params.approve, 10) ? true : false;
this.getActivityCourseIdIfNotSet(dataId, siteId, courseId).then((cId) => {
courseId = cId;
// Approve/disapprove entry.
return this.dataProvider.approveEntry(dataId, entryId, approve, courseId, siteId).catch((message) => {
this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errorapproving', true);
return Promise.reject(null);
});
}).then(() => {
const promises = [];
promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId));
promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId));
return Promise.all(promises);
}).then(() => {
this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, siteId);
this.domUtils.showToast(approve ? 'addon.mod_data.recordapproved' : 'addon.mod_data.recorddisapproved', true,
3000);
}).finally(() => {
modal.dismiss();
});
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
if (typeof params.d == 'undefined' || (typeof params.approve == 'undefined' && typeof params.disapprove == 'undefined')) {
// Required fields not defined. Cannot treat the URL.
return false;
}
return this.dataProvider.isPluginEnabled(siteId);
}
}

View File

@ -0,0 +1,907 @@
// (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 { CoreAppProvider } from '@providers/app';
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';
import { AddonModDataFieldsDelegate } from './fields-delegate';
/**
* 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,
private appProvider: CoreAppProvider, private fieldsDelegate: AddonModDataFieldsDelegate) {
this.logger = logger.getInstance('AddonModDataProvider');
}
/**
* Adds a new entry to a database.
*
* @param {number} dataId Data instance ID.
* @param {number} entryId EntryId or provisional entry ID when offline.
* @param {number} courseId Course ID.
* @param {any} contents The fields data to be created.
* @param {number} [groupId] Group id, 0 means that the function will determine the user group.
* @param {any} fields The fields that define the contents.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {boolean} [forceOffline] Force editing entry in offline.
* @return {Promise<any>} Promise resolved when the action is done.
*/
addEntry(dataId: number, entryId: number, courseId: number, contents: any, groupId: number = 0, fields: any, siteId?: string,
forceOffline: boolean = false): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Convenience function to store a data to be synchronized later.
const storeOffline = (): Promise<any> => {
return this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId)
.then((entry) => {
return {
// Return provissional entry Id.
newentryid: entry[1]
};
});
};
if (!this.appProvider.isOnline() || forceOffline) {
const notifications = this.checkFields(fields, contents);
if (notifications) {
return Promise.resolve({
fieldnotifications: notifications
});
}
}
return this.addEntryOnline(dataId, contents, groupId, siteId).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
}
// Couldn't connect to server, store in offline.
return storeOffline();
});
}
/**
* 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.
*
* @param {number} dataId Database ID.
* @param {number} entryId Entry ID.
* @param {boolean} approve Whether to approve (true) or unapprove the entry.
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the action is done.
*/
approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Convenience function to store a data to be synchronized later.
const storeOffline = (): Promise<any> => {
const action = approve ? 'approve' : 'disapprove';
return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId);
};
// Get if the opposite action is not synced.
const oppositeAction = approve ? 'disapprove' : 'approve';
return this.dataOffline.getEntry(dataId, entryId, oppositeAction, siteId).then(() => {
// Found. Just delete the action.
return this.dataOffline.deleteEntry(dataId, entryId, oppositeAction, siteId);
}).catch(() => {
if (!this.appProvider.isOnline()) {
// App is offline, store the action.
return storeOffline();
}
return this.approveEntryOnline(entryId, approve, siteId).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
}
// Couldn't connect to server, store in offline.
return storeOffline();
});
});
}
/**
* 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);
});
}
/**
* Convenience function to check fields requeriments here named "notifications".
*
* @param {any} fields The fields that define the contents.
* @param {any} contents The contents data of the fields.
* @return {any} Array of notifications if any or false.
*/
protected checkFields(fields: any, contents: any): any {
const notifications = [],
contentsIndexed = {};
contents.forEach((content) => {
if (typeof contentsIndexed[content.fieldid] == 'undefined') {
contentsIndexed[content.fieldid] = [];
}
contentsIndexed[content.fieldid].push(content);
});
// App is offline, check required fields.
fields.forEach((field) => {
const notification = this.fieldsDelegate.getFieldsNotifications(field, contentsIndexed[field.id]);
if (notification) {
notifications.push({
fieldname: field.name,
notification: notification
});
}
});
return notifications.length ? notifications : false;
}
/**
* Deletes an entry.
*
* @param {number} dataId Database ID.
* @param {number} entryId Entry ID.
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the action is done.
*/
deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Convenience function to store a data to be synchronized later.
const storeOffline = (): Promise<any> => {
return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId);
};
let justAdded = false;
// Check if the opposite action is not synced and just delete it.
return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => {
if (entries && entries.length) {
// Found. Delete other actions first.
const proms = entries.map((entry) => {
if (entry.action == 'add') {
justAdded = true;
}
return this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId);
});
return Promise.all(proms);
}
}).then(() => {
if (justAdded) {
// The field was added offline, delete and stop.
return;
}
if (!this.appProvider.isOnline()) {
// App is offline, store the action.
return storeOffline();
}
return this.deleteEntryOnline(entryId, siteId).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
}
// Couldn't connect to server, store in offline.
return storeOffline();
});
});
}
/**
* 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.
*
* @param {number} dataId Database ID.
* @param {number} entryId Entry ID.
* @param {number} courseId Course ID.
* @param {any} contents The contents data to be updated.
* @param {any} fields The fields that define the contents.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {boolean} forceOffline Force editing entry in offline.
* @return {Promise<any>} Promise resolved when the action is done.
*/
editEntry(dataId: number, entryId: number, courseId: number, contents: any, fields: any, siteId?: string,
forceOffline: boolean = false): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Convenience function to store a data to be synchronized later.
const storeOffline = (): Promise<any> => {
return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId)
.then(() => {
return {
updated: true
};
});
};
let justAdded = false,
groupId;
if (!this.appProvider.isOnline() || forceOffline) {
const notifications = this.checkFields(fields, contents);
if (notifications) {
return Promise.resolve({
fieldnotifications: notifications
});
}
}
// Get other not not synced actions.
return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => {
if (entries && entries.length) {
// Found. Delete add and edit actions first.
const proms = [];
entries.forEach((entry) => {
if (entry.action == 'add') {
justAdded = true;
groupId = entry.groupid;
proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId));
} else if (entry.action == 'edit') {
proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId));
}
});
return Promise.all(proms);
}
}).then(() => {
if (justAdded) {
// The field was added offline, add again and stop.
return this.addEntry(dataId, entryId, courseId, contents, groupId, fields, siteId, forceOffline)
.then((result) => {
result.updated = true;
return result;
});
}
if (!this.appProvider.isOnline() || forceOffline) {
// App is offline, store the action.
return storeOffline();
}
return this.editEntryOnline(entryId, contents, siteId).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
}
// Couldn't connect to server, store in offline.
return storeOffline();
});
});
}
/**
* 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);
});
}
/**
* Performs the whole fetch of the entries in the database.
*
* @param {number} dataId Data ID.
* @param {number} [groupId] Group ID.
* @param {string} [sort] Sort the records by this field id. See AddonModDataProvider#getEntries for more info.
* @param {string} [order] The direction of the sorting. See AddonModDataProvider#getEntries for more info.
* @param {number} [perPage] Records per page to fetch. It has to match with the prefetch.
* Default on AddonModDataProvider.PER_PAGE.
* @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false.
* @param {boolean} [ignoreCache] 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 done.
*/
fetchAllEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC',
perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false,
siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, [], 0, siteId);
}
/**
* Recursive call on fetch all entries.
*
* @param {number} dataId Data ID.
* @param {number} groupId Group ID.
* @param {string} sort Sort the records by this field id. See AddonModDataProvider#getEntries for more info.
* @param {string} order The direction of the sorting. See AddonModDataProvider#getEntries for more info.
* @param {number} perPage Records per page to fetch. It has to match with the prefetch.
* @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
* @param {boolean} ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
* @param {any} entries Entries already fetch (just to concatenate them).
* @param {number} page Page of records to return.
* @param {string} siteId Site ID.
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchEntriesRecursive(dataId: number, groupId: number, sort: string, order: string, perPage: number,
forceCache: boolean, ignoreCache: boolean, entries: any, page: number, siteId: string): Promise<any> {
return this.getEntries(dataId, groupId, sort, order, page, perPage, forceCache, ignoreCache, siteId)
.then((result) => {
entries = entries.concat(result.entries);
const canLoadMore = perPage > 0 && ((page + 1) * perPage) < result.totalcount;
if (canLoadMore) {
return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, entries, page + 1,
siteId);
}
return entries;
});
}
/**
* 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 AddonModDataProvider#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));
});
}
/**
* Invalidates database entry data.
*
* @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 data is invalidated.
*/
invalidateEntryData(dataId: number, entryId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getEntryCacheKey(dataId, entryId));
});
}
/**
* 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,118 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { AddonModDataProvider } from './data';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreEventsProvider } from '@providers/events';
/**
* Content links handler for database delete entry.
* Match mod/data/view.php?d=6&delete=5 with a valid data id and entryid.
*/
@Injectable()
export class AddonModDataDeleteLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonModDataDeleteLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModData';
pattern = /\/mod\/data\/view\.php.*([\?\&](d|delete)=\d+)/;
constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider,
private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider) {
super();
}
/**
* Convenience function to help get courseId.
*
* @param {number} dataId Database Id.
* @param {string} siteId Site Id, if not set, current site will be used.
* @param {number} courseId Course Id if already set.
* @return {Promise<number>} Resolved with course Id when done.
*/
protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise<number> {
if (courseId) {
return Promise.resolve(courseId);
}
return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => {
return module.course;
});
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?): void => {
const modal = this.domUtils.showModalLoading(),
dataId = parseInt(params.d, 10),
entryId = parseInt(params.delete, 10);
this.getActivityCourseIdIfNotSet(dataId, siteId, courseId).then((cId) => {
courseId = cId;
// Delete entry.
return this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId).catch((message) => {
this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true);
return Promise.reject(null);
});
}).then(() => {
const promises = [];
promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId));
promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId));
return Promise.all(promises);
}).then(() => {
this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId,
deleted: true}, siteId);
this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000);
}).finally(() => {
modal.dismiss();
});
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
if (typeof params.d == 'undefined' || typeof params.delete == 'undefined') {
// Required fields not defined. Cannot treat the URL.
return false;
}
return this.dataProvider.isPluginEnabled(siteId);
}
}

View File

@ -0,0 +1,92 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { AddonModDataProvider } from './data';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* Content links handler for database add or edit entry.
* Match mod/data/edit.php?d=6&rid=6 with a valid data and optional record id.
*/
@Injectable()
export class AddonModDataEditLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonModDataEditLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModData';
pattern = /\/mod\/data\/edit\.php.*([\?\&](d|rid)=\d+)/;
constructor(private linkHelper: CoreContentLinksHelperProvider, private dataProvider: AddonModDataProvider,
private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?): void => {
const modal = this.domUtils.showModalLoading(),
dataId = parseInt(params.d, 10),
rId = parseInt(params.rid, 10) || false;
this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => {
const pageParams = {
module: module,
courseId: module.course
};
if (rId) {
pageParams['entryId'] = rId;
}
return this.linkHelper.goInSite(navCtrl, 'AddonModDataEditPage', pageParams, siteId);
}).finally(() => {
// Just in case. In fact we need to dismiss the modal before showing a toast or error message.
modal.dismiss();
});
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
if (typeof params.d == 'undefined') {
// Id not defined. Cannot treat the URL.
return false;
}
return this.dataProvider.isPluginEnabled(siteId);
}
}

View File

@ -0,0 +1,222 @@
// (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> {
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) {
return originalContent;
}
return this.executeFunctionOnEnabled(field.type, 'overrideData', [originalContent || {}, offlineContent, offlineFiles]);
}
}

View File

@ -0,0 +1,494 @@
// (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 { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
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, protected dataProvider: AddonModDataProvider,
private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate,
private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider,
private textUtils: CoreTextUtilsProvider) { }
/**
* 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] =
this.textUtils.parseJSON(offlineContent.value);
} else {
offlineContents[offlineContent.fieldid][''] = this.textUtils.parseJSON(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 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
};
}
/**
* Fetch all entries and return it's Id
*
* @param {number} dataId Data ID.
* @param {number} groupId Group ID.
* @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false.
* @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. Current if not defined.
* @return {Promise<any>} Resolved with an array of entry ID.
*/
getAllEntriesIds(dataId: number, groupId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string):
Promise<any> {
return this.dataProvider.fetchAllEntries(dataId, groupId, undefined, undefined, undefined, forceCache, ignoreCache, siteId)
.then((entries) => {
return entries.map((entry) => entry.id);
});
}
/**
* Retrieve the entered data in the edit form.
* We don't use ng-model because it doesn't detect changes done by JavaScript.
*
* @param {any} inputData Array with the entered form values.
* @param {Array} fields Fields that defines every content in the entry.
* @param {number} [dataId] Database Id. If set, files will be uploaded and itemId set.
* @param {number} entryId Entry Id.
* @param {any} entryContents Original entry contents indexed by field id.
* @param {boolean} offline True to prepare the data for an offline uploading, false otherwise.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} That contains object with the answers.
*/
getEditDataFromForm(inputData: any, fields: any, dataId: number, entryId: number, entryContents: any, offline: boolean = false,
siteId?: string): Promise<any> {
if (!inputData) {
return Promise.resolve({});
}
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Filter and translate fields to each field plugin.
const edit = [],
promises = [];
fields.forEach((field) => {
promises.push(Promise.resolve(this.fieldsDelegate.getFieldEditData(field, inputData, entryContents[field.id]))
.then((fieldData) => {
if (fieldData) {
const proms = [];
fieldData.forEach((data) => {
let dataProm;
// Upload Files if asked.
if (dataId && data.files) {
dataProm = this.uploadOrStoreFiles(dataId, 0, entryId, data.fieldid, data.files, offline, siteId)
.then((filesResult) => {
delete data.files;
data.value = filesResult;
});
} else {
dataProm = Promise.resolve();
}
proms.push(dataProm.then(() => {
if (data.value) {
data.value = JSON.stringify(data.value);
}
if (typeof data.subfield == 'undefined') {
data.subfield = '';
}
// WS wants values in Json format.
edit.push(data);
}));
});
return Promise.all(proms);
}
}));
});
return Promise.all(promises).then(() => {
return edit;
});
}
/**
* Retrieve the temp files to be updated.
*
* @param {any} inputData Array with the entered form values.
* @param {Array} fields Fields that defines every content in the entry.
* @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set.
* @param {any} entryContents Original entry contents indexed by field id.
* @return {Promise<any>} That contains object with the files.
*/
getEditTmpFiles(inputData: any, fields: any, dataId: number, entryContents: any): Promise<any> {
if (!inputData) {
return Promise.resolve([]);
}
// Filter and translate fields to each field plugin.
const promises = fields.map((field) => {
return Promise.resolve(this.fieldsDelegate.getFieldEditFiles(field, inputData, entryContents[field.id]));
});
return Promise.all(promises).then((fieldsFiles) => {
return fieldsFiles.reduce((files: any[], fieldFiles: any) => files.concat(fieldFiles), []);
});
}
/**
* Get an online or offline entry.
*
* @param {any} data Database.
* @param {number} entryId Entry ID.
* @param {any} [offlineActions] Offline data with the actions done. Required for offline entries.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the entry.
*/
getEntry(data: any, entryId: number, offlineActions?: any, siteId?: string): Promise<any> {
if (entryId > 0) {
// It's an online entry, get it from WS.
return this.dataProvider.getEntry(data.id, entryId, siteId);
}
// It's an offline entry, search it in the offline actions.
return this.sitesProvider.getSite(siteId).then((site) => {
const offlineEntry = offlineActions.find((offlineAction) => offlineAction.action == 'add');
if (offlineEntry) {
const siteInfo = site.getInfo();
return {entry: {
id: offlineEntry.entryid,
canmanageentry: true,
approved: !data.approval || data.manageapproved,
dataid: offlineEntry.dataid,
groupid: offlineEntry.groupid,
timecreated: -offlineEntry.entryid,
timemodified: -offlineEntry.entryid,
userid: siteInfo.userid,
fullname: siteInfo.fullname,
contents: {}
}
};
}
});
}
/**
* Get page info related to an entry.
*
* @param {number} dataId Data ID.
* @param {number} entryId Entry ID.
* @param {number} groupId Group ID.
* @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false.
* @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. Current if not defined.
* @return {Promise<any>} Containing page number, if has next and have following page.
*/
getPageInfoByEntry(dataId: number, entryId: number, groupId: number, forceCache: boolean = false,
ignoreCache: boolean = false, siteId?: string): Promise<any> {
return this.getAllEntriesIds(dataId, groupId, forceCache, ignoreCache, siteId).then((entries) => {
const index = entries.findIndex((entry) => entry == entryId);
if (index >= 0) {
return {
previousId: entries[index - 1] || false,
nextId: entries[index + 1] || false,
entryId: entryId,
page: index + 1, // Parsed to natural language.
numEntries: entries.length
};
}
return false;
});
}
/**
* Get page info related to an entry by page number.
*
* @param {number} dataId Data ID.
* @param {number} page Page number.
* @param {number} groupId Group ID.
* @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false.
* @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. Current if not defined.
* @return {Promise<any>} Containing page number, if has next and have following page.
*/
getPageInfoByPage(dataId: number, page: number, groupId: number, forceCache: boolean = false,
ignoreCache: boolean = false, siteId?: string): Promise<any> {
return this.getAllEntriesIds(dataId, groupId, forceCache, ignoreCache, siteId).then((entries) => {
const index = page - 1,
entryId = entries[index];
if (entryId) {
return {
previousId: entries[index - 1] || null,
nextId: entries[index + 1] || null,
entryId: entryId,
page: page, // Parsed to natural language.
numEntries: entries.length
};
}
return false;
});
}
/**
* 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 [];
});
});
}
/**
* Check if data has been changed by the user.
*
* @param {any} inputData Array with the entered form values.
* @param {any} fields Fields that defines every content in the entry.
* @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set.
* @param {any} entryContents Original entry contents indexed by field id.
* @return {Promise<boolean>} True if changed, false if not.
*/
hasEditDataChanged(inputData: any, fields: any, dataId: number, entryContents: any): Promise<boolean> {
const promises = fields.map((field) => {
return this.fieldsDelegate.hasFieldDataChanged(field, inputData, entryContents[field.id]);
});
// Will reject on first change detected.
return Promise.all(promises).then(() => {
// No changes.
return false;
}).catch(() => {
// Has changes.
return true;
});
}
/**
* 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, 'AddonModData', '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,244 @@
// (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 DATA_ENTRY_TABLE = 'addon_mod_data_entry';
protected tablesSchema = [
{
name: this.DATA_ENTRY_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.DATA_ENTRY_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.DATA_ENTRY_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.DATA_ENTRY_TABLE, {dataid: dataId});
});
}
/**
* Get an stored entry data.
*
* @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 with entry.
*/
getEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecord(this.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId, action: action});
});
}
/**
* 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.DATA_ENTRY_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);
});
}
/**
* Save an entry data to be sent later.
*
* @param {number} dataId Database ID.
* @param {number} entryId Database entry Id. If action is add entryId should be 0 and -timemodified will be used.
* @param {string} action Action to be done to the entry: [add, edit, delete, approve, disapprove]
* @param {number} courseId Course ID of the database.
* @param {number} [groupId] Group ID. Only provided when adding.
* @param {any[]} [fields] Array of field data of the entry if needed.
* @param {number} [timemodified] The time the entry was modified. If not defined, current time.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
*/
saveEntry(dataId: number, entryId: number, action: string, courseId: number, groupId?: number, fields?: any[],
timemodified?: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
timemodified = timemodified || new Date().getTime();
entryId = typeof entryId == 'undefined' || entryId === null ? -timemodified : entryId;
const entry = {
dataid: dataId,
courseid: courseId,
groupid: groupId,
action: action,
entryid: entryId,
fields: fields,
timemodified: timemodified
};
return site.getDb().insertRecord(this.DATA_ENTRY_TABLE, entry);
});
}
}

View File

@ -0,0 +1,285 @@
// (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 { CoreFilepoolProvider } from '@providers/filepool';
import { CoreGroupsProvider } from '@providers/groups';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreCommentsProvider } from '@core/comments/providers/comments';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
import { AddonModDataProvider } from './data';
import { AddonModDataHelperProvider } from './helper';
/**
* Handler to prefetch databases.
*/
@Injectable()
export class AddonModDataPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
name = 'AddonModData';
modName = 'data';
component = AddonModDataProvider.COMPONENT;
updatesNames = /^configuration$|^.*files$|^entries$|^gradeitems$|^outcomes$|^comments$|^ratings/;
constructor(injector: Injector, protected dataProvider: AddonModDataProvider, protected timeUtils: CoreTimeUtilsProvider,
protected filepoolProvider: CoreFilepoolProvider, protected dataHelper: AddonModDataHelperProvider,
protected groupsProvider: CoreGroupsProvider, protected commentsProvider: CoreCommentsProvider,
protected courseProvider: CoreCourseProvider) {
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 = [],
siteId = this.sitesProvider.getCurrentSiteId();
promises.push(super.downloadOrPrefetch(module, courseId, prefetch));
promises.push(this.getDatabaseInfoHelper(module, courseId, false, false, true, siteId).then((info) => {
// Prefetch the database data.
const database = info.database,
promises = [];
promises.push(this.dataProvider.getFields(database.id, false, true, siteId));
promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id));
info.groups.forEach((group) => {
promises.push(this.dataProvider.getDatabaseAccessInformation(database.id, group.id, false, true, siteId));
});
info.entries.forEach((entry) => {
promises.push(this.dataProvider.getEntry(database.id, entry.id, siteId));
if (database.comments) {
promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id,
'database_entry', 0, siteId));
}
});
// Add Basic Info to manage links.
promises.push(this.courseProvider.getModuleBasicInfoByInstance(database.id, 'data', siteId));
return Promise.all(promises);
}));
return Promise.all(promises);
}
/**
* Retrieves all the entries for all the groups and then returns only unique entries.
*
* @param {number} dataId Database Id.
* @param {any[]} groups Array of groups in the activity.
* @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false.
* @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID.
* @return {Promise<any>} All unique entries.
*/
protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false,
siteId?: string): Promise<any> {
const promises = groups.map((group) => {
return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache,
siteId);
});
return Promise.all(promises).then((responses) => {
const uniqueEntries = {};
responses.forEach((groupEntries) => {
groupEntries.forEach((entry) => {
uniqueEntries[entry.id] = entry;
});
});
return this.utils.objectToArray(uniqueEntries);
});
}
/**
* Helper function to get all database info just once.
*
* @param {any} module Module to get the files.
* @param {number} courseId Course ID the module belongs to.
* @param {boolean} [omitFail] True to always return even if fails. Default false.
* @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false.
* @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
* @param {string} siteId Site ID.
* @return {Promise<any>} Promise resolved with the info fetched.
*/
protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false,
ignoreCache: boolean = false, siteId?: string): Promise<any> {
let database,
groups = [],
entries = [],
files = [];
siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.dataProvider.getDatabase(courseId, module.id, siteId, forceCache).then((data) => {
files = this.getIntroFilesFromInstance(module, data);
database = data;
return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => {
if (!groupInfo.groups || groupInfo.groups.length == 0) {
groupInfo.groups = [{id: 0}];
}
groups = groupInfo.groups;
return this.getAllUniqueEntries(database.id, groups, forceCache, ignoreCache, siteId);
});
}).then((uniqueEntries) => {
entries = uniqueEntries;
files = files.concat(this.getEntriesFiles(entries));
return {
database: database,
groups: groups,
entries: entries,
files: files
};
}).catch((message): any => {
if (omitFail) {
// Any error, return the info we have.
return {
database: database,
groups: groups,
entries: entries,
files: files
};
}
return Promise.reject(message);
});
}
/**
* Returns the file contained in the entries.
*
* @param {any[]} entries List of entries to get files from.
* @return {any[]} List of files.
*/
protected getEntriesFiles(entries: any[]): any[] {
let files = [];
entries.forEach((entry) => {
entry.contents.forEach((content) => {
files = files.concat(content.files);
});
});
return files;
}
/**
* Get the list of downloadable files.
*
* @param {any} module Module to get the files.
* @param {number} courseId Course ID the module belongs to.
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
* @return {Promise<any>} Promise resolved with the list of files.
*/
getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> {
return this.getDatabaseInfoHelper(module, courseId, true).then((info) => {
return info.files;
});
}
/**
* 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> {
const promises = [];
promises.push(this.dataProvider.invalidateDatabaseData(courseId));
promises.push(this.dataProvider.invalidateDatabaseAccessInformationData(module.instance));
return Promise.all(promises);
}
/**
* Check if a database is downloadable.
* A database isn't downloadable if it's not open yet.
*
* @param {any} module Module to check.
* @param {number} courseId Course ID the module belongs to.
* @return {Promise<any>} Promise resolved with true if downloadable, resolved with false otherwise.
*/
isDownloadable(module: any, courseId: number): boolean | Promise<boolean> {
return this.dataProvider.getDatabase(courseId, module.id, undefined, true).then((database) => {
return this.dataProvider.getDatabaseAccessInformation(database.id).then((accessData) => {
// Check if database is restricted by time.
if (!accessData.timeavailable) {
const time = this.timeUtils.timestamp();
// It is restricted, checking times.
if (database.timeavailablefrom && time < database.timeavailablefrom) {
return false;
}
if (database.timeavailableto && time > database.timeavailableto) {
return false;
}
}
return true;
});
});
}
/**
* 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,104 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { AddonModDataProvider } from './data';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* Content links handler for database show entry.
* Match mod/data/view.php?d=6&rid=5 with a valid data id and entryid.
*/
@Injectable()
export class AddonModDataShowLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonModDataShowLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModData';
pattern = /\/mod\/data\/view\.php.*([\?\&](d|rid|page|group|mode)=\d+)/;
constructor(private linkHelper: CoreContentLinksHelperProvider, private dataProvider: AddonModDataProvider,
private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?): void => {
const modal = this.domUtils.showModalLoading(),
dataId = parseInt(params.d, 10),
rId = parseInt(params.rid, 10) || false,
group = parseInt(params.group, 10) || false,
page = parseInt(params.page, 10) || false;
this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => {
const pageParams = {
module: module,
courseId: module.course
};
if (group) {
pageParams['group'] = group;
}
if (params.mode && params.mode == 'single') {
pageParams['page'] = page || 1;
} else if (rId) {
pageParams['entryId'] = rId;
}
return this.linkHelper.goInSite(navCtrl, 'AddonModDataEntryPage', pageParams, siteId);
}).finally(() => {
// Just in case. In fact we need to dismiss the modal before showing a toast or error message.
modal.dismiss();
});
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
if (typeof params.d == 'undefined') {
// Id not defined. Cannot treat the URL.
return false;
}
if ((!params.mode || params.mode != 'single') && typeof params.rid == 'undefined') {
return false;
}
return this.dataProvider.isPluginEnabled(siteId);
}
}

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 = this.textUtils.parseJSON(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

@ -47,7 +47,6 @@ export class AddonModFeedbackHelperProvider {
protected getActivityHistoryBackCounter(pageName: string, instance: number, paramName: string, prefix: string,
navCtrl: NavController): number {
let historyInstance, params,
backTimes = 0,
view = navCtrl.getActive();
while (!view.isFirst()) {
@ -60,9 +59,7 @@ export class AddonModFeedbackHelperProvider {
historyInstance = params.get(paramName) ? params.get(paramName) : params.get('module').instance;
// Check we are not changing to another activity.
if (historyInstance && historyInstance == instance) {
backTimes++;
} else {
if (!historyInstance || historyInstance != instance) {
break;
}

View File

@ -15,7 +15,6 @@
import { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { AddonModFeedbackProvider } from './feedback';
/**
* Handler to treat links to feedback.
@ -25,6 +24,6 @@ export class AddonModFeedbackLinkHandler extends CoreContentLinksModuleIndexHand
name = 'AddonModFeedbackLinkHandler';
constructor(courseHelper: CoreCourseHelperProvider) {
super(courseHelper, AddonModFeedbackProvider.COMPONENT, 'feedback');
super(courseHelper, 'AddonModFeedback', 'feedback');
}
}

View File

@ -124,9 +124,9 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan
/**
* Get the list of downloadable files.
*
* @param {any} module Module to get the files.
* @param {any} module Module to get the files.
* @param {number} courseId Course ID the module belongs to.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
* @return {Promise<any>} Promise resolved with the list of files.
*/
getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> {

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

@ -6,5 +6,5 @@
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form">
<ion-label stacked [core-mark-required]="field.required">{{ field.name }}</ion-label>
<ion-datetime [formControlName]="field.modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="field.format" core-input-errors [max]="field.max" [min]="field.min"></ion-datetime>
<ion-datetime [formControlName]="field.modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="field.format" [max]="field.max" [min]="field.min"></ion-datetime>
</ion-item>

View File

@ -6,7 +6,7 @@
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form">
<ion-label stacked [core-mark-required]="field.required">{{ field.name }}</ion-label>
<ion-select [formControlName]="field.modelName" [placeholder]="'core.choosedots' | translate" core-input-errors interface="popover">
<ion-select [formControlName]="field.modelName" [placeholder]="'core.choosedots' | translate" interface="popover">
<ion-option value="">{{ 'core.choosedots' | translate }}</ion-option>
<ion-option *ngFor="let option of field.options" [value]="option">{{option}}</ion-option>
</ion-select>

View File

@ -6,5 +6,5 @@
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form">
<ion-label stacked [core-mark-required]="field.required">{{ field.name }}</ion-label>
<ion-input [type]="field.inputType" [formControlName]="field.modelName" [placeholder]="field.name" maxlength="{{field.maxlength}}" core-input-errors></ion-input>
<ion-input [type]="field.inputType" [formControlName]="field.modelName" [placeholder]="field.name" maxlength="{{field.maxlength}}"></ion-input>
</ion-item>

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';
@ -186,6 +187,7 @@ export const CORE_PROVIDERS: any[] = [
AddonModBookModule,
AddonModChatModule,
AddonModChoiceModule,
AddonModDataModule,
AddonModLabelModule,
AddonModLessonModule,
AddonModResourceModule,

View File

@ -171,6 +171,10 @@ ion-avatar ion-img, ion-avatar img {
font-style: italic;
}
ion-datetime {
position: relative;
}
/** Format Text */
core-format-text[maxHeight], *[core-format-text][maxHeight],
core-format-text[ng-reflect-max-height], *[core-format-text][ng-reflect-max-height] {
@ -296,14 +300,25 @@ core-format-text, *[core-format-text] {
}
// Fix lists styles in core-format-text.
ul, ol {
-webkit-padding-start: 40px;
}
ul {
list-style: disc;
list-style-type: disc;
}
ol {
list-style: decimal;
list-style-type: decimal;
}
ul, ol {
-webkit-padding-start: 15px;
list-style-position: inside;
ul {
list-style-type: circle;
}
ol {
list-style-type: lower-latin;
}
ul, ol {
list-style-position: inside;
margin-left: 15px;
}
}
.badge {

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

@ -18,6 +18,7 @@ import {
} from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* Component to create another component dynamically.
@ -68,7 +69,9 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck {
protected differ: any; // To detect changes in the data input.
constructor(logger: CoreLoggerProvider, protected factoryResolver: ComponentFactoryResolver, differs: KeyValueDiffers,
@Optional() protected navCtrl: NavController, protected cdr: ChangeDetectorRef, protected element: ElementRef) {
@Optional() protected navCtrl: NavController, protected cdr: ChangeDetectorRef, protected element: ElementRef,
protected domUtils: CoreDomUtilsProvider) {
this.logger = logger.getInstance('CoreDynamicComponent');
this.differ = differs.find([]).create();
}
@ -99,7 +102,7 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck {
if (changes) {
this.setInputData();
if (this.instance.ngOnChanges) {
this.instance.ngOnChanges(this.createChangesForComponent(changes));
this.instance.ngOnChanges(this.domUtils.createChangesFromKeyValueDiff(changes));
}
}
}
@ -170,29 +173,4 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck {
this.instance[name] = this.data[name];
}
}
/**
* Given the changes on the data input, create the changes object for the component.
*
* @param {any} changes Changes in the data input (detected by KeyValueDiffer).
* @return {{[name: string]: SimpleChange}} List of changes for the component.
*/
protected createChangesForComponent(changes: any): { [name: string]: SimpleChange } {
const newChanges: { [name: string]: SimpleChange } = {};
// Added items are considered first change.
changes.forEachAddedItem((item) => {
newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true);
});
// Changed or removed items aren't first change.
changes.forEachChangedItem((item) => {
newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, false);
});
changes.forEachRemovedItem((item) => {
newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true);
});
return newChanges;
}
}

View File

@ -1,5 +1,8 @@
<div class="core-input-error-container" *ngIf="formControl.dirty && !formControl.valid" role="alert">
<ng-container *ngFor="let error of errorKeys">
<div *ngIf="formControl.hasError(error)" class="core-input-error">{{errorMessages[error]}}</div>
<div class="core-input-error-container" role="alert" *ngIf="(formControl && formControl.dirty && !formControl.valid) || errorText">
<ng-container *ngIf="formControl && formControl.dirty && !formControl.valid">
<ng-container *ngFor="let error of errorKeys">
<div *ngIf="formControl.hasError(error)" class="core-input-error">{{errorMessages[error]}}</div>
</ng-container>
</ng-container>
<div *ngIf="errorText" class="core-input-error">{{ errorText }}</div>
</div>

Some files were not shown because too many files have changed in this diff Show More