commit
7ef4726bf5
|
@ -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" />
|
||||
|
|
|
@ -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. -->
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -0,0 +1,5 @@
|
|||
<core-dynamic-component [component]="fieldComponent" [data]="data">
|
||||
<!-- This content will be replaced by the component if found. -->
|
||||
<core-loading [hideUntil]="fieldLoaded">
|
||||
</core-loading>
|
||||
</core-dynamic-component>
|
|
@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AddonModDataFieldCheckboxHandler } from './providers/handler';
|
||||
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
|
||||
import { AddonModDataFieldCheckboxComponent } from './component/checkbox';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModDataFieldCheckboxComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonModDataFieldCheckboxHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModDataFieldCheckboxComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModDataFieldCheckboxComponent
|
||||
]
|
||||
})
|
||||
export class AddonModDataFieldCheckboxModule {
|
||||
constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldCheckboxHandler) {
|
||||
fieldDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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>
|
|
@ -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>');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 { }
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>');
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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/>
|
|
@ -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 || '';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<!-- The buttons defined by the component will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="dataComponent.loaded" (ionRefresh)="dataComponent.doRefresh($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<addon-mod-data-index [module]="module" [courseId]="courseId" [group]="group" (dataRetrieved)="updateData($event)"></addon-mod-data-index>
|
||||
</ion-content>
|
|
@ -0,0 +1,33 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModDataComponentsModule } from '../../components/components.module';
|
||||
import { AddonModDataIndexPage } from './index';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModDataIndexPage,
|
||||
],
|
||||
imports: [
|
||||
CoreDirectivesModule,
|
||||
AddonModDataComponentsModule,
|
||||
IonicPageModule.forChild(AddonModDataIndexPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModDataIndexPageModule {}
|
|
@ -0,0 +1,50 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { IonicPage, NavParams } from 'ionic-angular';
|
||||
import { AddonModDataIndexComponent } from '../../components/index/index';
|
||||
|
||||
/**
|
||||
* Page that displays a data.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-data-index' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-data-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModDataIndexPage {
|
||||
@ViewChild(AddonModDataIndexComponent) dataComponent: AddonModDataIndexComponent;
|
||||
|
||||
title: string;
|
||||
module: any;
|
||||
courseId: number;
|
||||
group: number;
|
||||
|
||||
constructor(navParams: NavParams) {
|
||||
this.module = navParams.get('module') || {};
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.group = navParams.get('group') || 0;
|
||||
this.title = this.module.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update some data based on the data instance.
|
||||
*
|
||||
* @param {any} data Data instance.
|
||||
*/
|
||||
updateData(data: any): void {
|
||||
this.title = data.name || this.title;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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>
|
|
@ -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 {}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AddonModDataFieldHandler } from './fields-delegate';
|
||||
|
||||
/**
|
||||
* Default handler used when a field plugin doesn't have a specific implementation.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModDataDefaultFieldHandler implements AddonModDataFieldHandler {
|
||||
name = 'AddonModDataDefaultFieldHandler';
|
||||
type = 'default';
|
||||
|
||||
/**
|
||||
* Get field search data in the input data.
|
||||
*
|
||||
* @param {any} field Defines the field to be rendered.
|
||||
* @param {any} inputData Data entered in the search form.
|
||||
* @return {any} With name and value of the data to be sent.
|
||||
*/
|
||||
getFieldSearchData(field: any, inputData: any): any {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field edit data in the input data.
|
||||
*
|
||||
* @param {any} field Defines the field to be rendered.
|
||||
* @param {any} inputData Data entered in the edit form.
|
||||
* @return {any} With name and value of the data to be sent.
|
||||
*/
|
||||
getFieldEditData(field: any, inputData: any, originalFieldData: any): any {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field data in changed.
|
||||
*
|
||||
* @param {any} field Defines the field to be rendered.
|
||||
* @param {any} inputData Data entered in the edit form.
|
||||
* @param {any} originalFieldData Original field entered data.
|
||||
* @return {Promise<boolean> | boolean} If the field has changes.
|
||||
*/
|
||||
hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise<boolean> | boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field edit files in the input data.
|
||||
*
|
||||
* @param {any} field Defines the field..
|
||||
* @return {any} With name and value of the data to be sent.
|
||||
*/
|
||||
getFieldEditFiles(field: any, inputData: any, originalFieldData: any): any {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and get field requeriments.
|
||||
*
|
||||
* @param {any} field Defines the field to be rendered.
|
||||
* @param {any} inputData Data entered in the edit form.
|
||||
* @return {string | false} String with the notification or false.
|
||||
*/
|
||||
getFieldsNotifications(field: any, inputData: any): string | false {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override field content data with offline submission.
|
||||
*
|
||||
* @param {any} originalContent Original data to be overriden.
|
||||
* @param {any} offlineContent Array with all the offline data to override.
|
||||
* @param {any} [offlineFiles] Array with all the offline files in the field.
|
||||
* @return {any} Data overriden
|
||||
*/
|
||||
overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any {
|
||||
return originalContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NavController, NavOptions } from 'ionic-angular';
|
||||
import { AddonModDataIndexComponent } from '../components/index/index';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { AddonModDataProvider } from './data';
|
||||
|
||||
/**
|
||||
* Handler to support data modules.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModDataModuleHandler implements CoreCourseModuleHandler {
|
||||
name = 'AddonModData';
|
||||
modName = 'data';
|
||||
|
||||
constructor(private courseProvider: CoreCourseProvider, private dataProvider: AddonModDataProvider) { }
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return {Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return this.dataProvider.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data required to display the module in the course contents view.
|
||||
*
|
||||
* @param {any} module The module object.
|
||||
* @param {number} courseId The course ID.
|
||||
* @param {number} sectionId The section ID.
|
||||
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
||||
*/
|
||||
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
||||
return {
|
||||
icon: this.courseProvider.getModuleIconSrc('data'),
|
||||
title: module.name,
|
||||
class: 'addon-mod_data-handler',
|
||||
showDownloadButton: true,
|
||||
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||
navCtrl.push('AddonModDataIndexPage', {module: module, courseId: courseId}, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||
* The component returned must implement CoreCourseModuleMainComponent.
|
||||
*
|
||||
* @param {any} course The course object.
|
||||
* @param {any} module The module object.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getMainComponent(course: any, module: any): any {
|
||||
return AddonModDataIndexComponent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreCronHandler } from '@providers/cron';
|
||||
import { AddonModDataSyncProvider } from './sync';
|
||||
|
||||
/**
|
||||
* Synchronization cron handler.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModDataSyncCronHandler implements CoreCronHandler {
|
||||
name = 'AddonModDataSyncCronHandler';
|
||||
|
||||
constructor(private dataSync: AddonModDataSyncProvider) {}
|
||||
|
||||
/**
|
||||
* Execute the process.
|
||||
* Receives the ID of the site affected, undefined for all sites.
|
||||
*
|
||||
* @param {string} [siteId] ID of the site affected, undefined for all sites.
|
||||
* @return {Promise<any>} Promise resolved when done, rejected if failure.
|
||||
*/
|
||||
execute(siteId?: string): Promise<any> {
|
||||
return this.dataSync.syncAllDatabases(siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time between consecutive executions.
|
||||
*
|
||||
* @return {number} Time between consecutive executions (in ms).
|
||||
*/
|
||||
getInterval(): number {
|
||||
return 600000; // 10 minutes.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,337 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { AddonModDataOfflineProvider } from './offline';
|
||||
import { AddonModDataProvider } from './data';
|
||||
import { AddonModDataHelperProvider } from './helper';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
|
||||
/**
|
||||
* Service to sync databases.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
|
||||
|
||||
static AUTO_SYNCED = 'addon_mod_data_autom_synced';
|
||||
protected componentTranslate: string;
|
||||
|
||||
constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
|
||||
protected appProvider: CoreAppProvider, private dataOffline: AddonModDataOfflineProvider,
|
||||
private eventsProvider: CoreEventsProvider, private dataProvider: AddonModDataProvider,
|
||||
protected translate: TranslateService, private utils: CoreUtilsProvider, courseProvider: CoreCourseProvider,
|
||||
syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider,
|
||||
private dataHelper: AddonModDataHelperProvider) {
|
||||
super('AddonModDataSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||
this.componentTranslate = courseProvider.translateModuleName('data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a database has data to synchronize.
|
||||
*
|
||||
* @param {number} dataId Database ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: true if has data to sync, false otherwise.
|
||||
*/
|
||||
hasDataToSync(dataId: number, siteId?: string): Promise<boolean> {
|
||||
return this.dataOffline.hasOfflineData(dataId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize all the databases in a certain site or in all sites.
|
||||
*
|
||||
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
syncAllDatabases(siteId?: string): Promise<any> {
|
||||
return this.syncOnSites('all databases', this.syncAllDatabasesFunc.bind(this), undefined, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all pending databases on a site.
|
||||
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||
* @param {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected syncAllDatabasesFunc(siteId?: string): Promise<any> {
|
||||
// Get all data answers pending to be sent in the site.
|
||||
return this.dataOffline.getAllEntries(siteId).then((offlineActions) => {
|
||||
const promises = {};
|
||||
|
||||
// Do not sync same database twice.
|
||||
offlineActions.forEach((action) => {
|
||||
if (typeof promises[action.dataid] != 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
promises[action.dataid] = this.syncDatabaseIfNeeded(action.dataid, siteId)
|
||||
.then((result) => {
|
||||
if (result && result.updated) {
|
||||
// Sync done. Send event.
|
||||
this.eventsProvider.trigger(AddonModDataSyncProvider.AUTO_SYNCED, {
|
||||
dataId: action.dataid,
|
||||
warnings: result.warnings
|
||||
}, siteId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Promises will be an object so, convert to an array first;
|
||||
return Promise.all(this.utils.objectToArray(promises));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a database only if a certain time has passed since the last time.
|
||||
*
|
||||
* @param {number} dataId Database ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is synced or if it doesn't need to be synced.
|
||||
*/
|
||||
syncDatabaseIfNeeded(dataId: number, siteId?: string): Promise<any> {
|
||||
return this.isSyncNeeded(dataId, siteId).then((needed) => {
|
||||
if (needed) {
|
||||
return this.syncDatabase(dataId, siteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize a data.
|
||||
*
|
||||
* @param {number} dataId Data ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
syncDatabase(dataId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
if (this.isSyncing(dataId, siteId)) {
|
||||
// There's already a sync ongoing for this data and user, return the promise.
|
||||
return this.getOngoingSync(dataId, siteId);
|
||||
}
|
||||
|
||||
// Verify that data isn't blocked.
|
||||
if (this.syncProvider.isBlocked(AddonModDataProvider.COMPONENT, dataId, siteId)) {
|
||||
this.logger.debug(`Cannot sync database '${dataId}' because it is blocked.`);
|
||||
|
||||
return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
|
||||
}
|
||||
|
||||
this.logger.debug(`Try to sync data '${dataId}' in site ${siteId}'`);
|
||||
|
||||
let courseId,
|
||||
data;
|
||||
const result = {
|
||||
warnings: [],
|
||||
updated: false
|
||||
};
|
||||
|
||||
// Get answers to be sent.
|
||||
const syncPromise = this.dataOffline.getDatabaseEntries(dataId, siteId).catch(() => {
|
||||
// No offline data found, return empty object.
|
||||
return [];
|
||||
}).then((offlineActions) => {
|
||||
if (!offlineActions.length) {
|
||||
// Nothing to sync.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.appProvider.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
courseId = offlineActions[0].courseid;
|
||||
|
||||
// Send the answers.
|
||||
return this.dataProvider.getDatabaseById(courseId, dataId, siteId).then((database) => {
|
||||
data = database;
|
||||
|
||||
const offlineEntries = {};
|
||||
|
||||
offlineActions.forEach((entry) => {
|
||||
if (typeof offlineEntries[entry.entryid] == 'undefined') {
|
||||
offlineEntries[entry.entryid] = [];
|
||||
}
|
||||
offlineEntries[entry.entryid].push(entry);
|
||||
});
|
||||
|
||||
const promises = this.utils.objectToArray(offlineEntries).map((entryActions) => {
|
||||
return this.syncEntry(data, entryActions, result, siteId);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(() => {
|
||||
if (result.updated) {
|
||||
// Data has been sent to server. Now invalidate the WS calls.
|
||||
return this.dataProvider.invalidateContent(data.cmid, courseId, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
// Sync finished, set sync time.
|
||||
return this.setSyncTime(dataId, siteId);
|
||||
}).then(() => {
|
||||
return result;
|
||||
});
|
||||
|
||||
return this.addOngoingSync(dataId, syncPromise, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize an entry.
|
||||
*
|
||||
* @param {any} data Database.
|
||||
* @param {any} entryActions Entry actions.
|
||||
* @param {any} result Object with the result of the sync.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected syncEntry(data: any, entryActions: any[], result: any, siteId?: string): Promise<any> {
|
||||
let discardError,
|
||||
timePromise,
|
||||
entryId = 0,
|
||||
offlineId,
|
||||
deleted = false;
|
||||
|
||||
const promises = [];
|
||||
|
||||
// Sort entries by timemodified.
|
||||
entryActions = entryActions.sort((a: any, b: any) => a.timemodified - b.timemodified);
|
||||
|
||||
entryId = entryActions[0].entryid;
|
||||
|
||||
if (entryId > 0) {
|
||||
timePromise = this.dataProvider.getEntry(data.id, entryId, siteId).then((entry) => {
|
||||
return entry.entry.timemodified;
|
||||
}).catch(() => {
|
||||
return -1;
|
||||
});
|
||||
} else {
|
||||
offlineId = entryId;
|
||||
timePromise = Promise.resolve(0);
|
||||
}
|
||||
|
||||
return timePromise.then((timemodified) => {
|
||||
if (timemodified < 0 || timemodified >= entryActions[0].timemodified) {
|
||||
// The entry was not found in Moodle or the entry has been modified, discard the action.
|
||||
result.updated = true;
|
||||
discardError = this.translate.instant('addon.mod_data.warningsubmissionmodified');
|
||||
|
||||
return this.dataOffline.deleteAllEntryActions(data.id, entryId, siteId);
|
||||
}
|
||||
|
||||
entryActions.forEach((action) => {
|
||||
let actionPromise;
|
||||
const proms = [];
|
||||
|
||||
entryId = action.entryid > 0 ? action.entryid : entryId;
|
||||
|
||||
if (action.fields) {
|
||||
action.fields.forEach((field) => {
|
||||
// Upload Files if asked.
|
||||
const value = 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[]> {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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).
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue