diff --git a/config.xml b/config.xml index 5b9064b2c..8d93693b2 100644 --- a/config.xml +++ b/config.xml @@ -13,6 +13,10 @@ + + + + diff --git a/src/addon/mod/assign/components/submission/submission.html b/src/addon/mod/assign/components/submission/submission.html index 8d4cb9ffc..11becfd51 100644 --- a/src/addon/mod/assign/components/submission/submission.html +++ b/src/addon/mod/assign/components/submission/submission.html @@ -153,7 +153,7 @@ {{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo.grade} }} - + diff --git a/src/addon/mod/data/classes/field-plugin-component.ts b/src/addon/mod/data/classes/field-plugin-component.ts new file mode 100644 index 000000000..349ac1be7 --- /dev/null +++ b/src/addon/mod/data/classes/field-plugin-component.ts @@ -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; + } +} diff --git a/src/addon/mod/data/components/action/action.html b/src/addon/mod/data/components/action/action.html new file mode 100644 index 000000000..53f5096d7 --- /dev/null +++ b/src/addon/mod/data/components/action/action.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +{{ entry.timecreated * 1000 | coreFormatDate:"dffulldate" }} +{{ entry.timemodified * 1000 | coreFormatDate:"dffulldate" }} + + + + + +{{entry.fullname}} diff --git a/src/addon/mod/data/components/action/action.ts b/src/addon/mod/data/components/action/action.ts new file mode 100644 index 000000000..0cdb76f45 --- /dev/null +++ b/src/addon/mod/data/components/action/action.ts @@ -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} Solved when done. + */ + undoDelete(): Promise { + 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; + } + } +} diff --git a/src/addon/mod/data/components/components.module.ts b/src/addon/mod/data/components/components.module.ts new file mode 100644 index 000000000..3470ae872 --- /dev/null +++ b/src/addon/mod/data/components/components.module.ts @@ -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 {} diff --git a/src/addon/mod/data/components/field-plugin/field-plugin.html b/src/addon/mod/data/components/field-plugin/field-plugin.html new file mode 100644 index 000000000..489bbda21 --- /dev/null +++ b/src/addon/mod/data/components/field-plugin/field-plugin.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/components/field-plugin/field-plugin.ts b/src/addon/mod/data/components/field-plugin/field-plugin.ts new file mode 100644 index 000000000..c2ae6751f --- /dev/null +++ b/src/addon/mod/data/components/field-plugin/field-plugin.ts @@ -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; + } + } + } +} diff --git a/src/addon/mod/data/components/index/index.html b/src/addon/mod/data/components/index/index.html new file mode 100644 index 000000000..ce67bfa94 --- /dev/null +++ b/src/addon/mod/data/components/index/index.html @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} +
+ + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + +
+ + {{ 'addon.mod_data.notopenyet' | translate:{$a: timeAvailableFromReadable} }} +
+ +
+ + {{ 'addon.mod_data.expired' | translate:{$a: timeAvailableToReadable} }} +
+ +
+ + {{ 'addon.mod_data.entrieslefttoaddtoview' | translate:{$a: {entrieslefttoview: access.entrieslefttoview} } }} +
+ +
+ + {{ 'addon.mod_data.entrieslefttoadd' | translate:{$a: {entriesleft: access.entrieslefttoadd} } }} +
+ + + {{ 'addon.mod_data.resetsettings' | translate}} + + +
+ + + +
+ + + + + + + + + + + + + +
+ +
+
+ + + {{ 'addon.mod_data.resetsettings' | translate}} + + + +
diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts new file mode 100644 index 000000000..409bf230f --- /dev/null +++ b/src/addon/mod/data/components/index/index.ts @@ -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} Resolved when done. + */ + protected invalidateContent(): Promise { + 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} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + 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} Resolved then done. + */ + protected fetchEntriesData(): Promise { + 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} Resolved when done. + */ + searchEntries(page: number): Promise { + 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} Resolved when new group is selected or rejected if not. + */ + setGroup(groupId: number): Promise { + 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} Resolved then done. + */ + protected fetchOfflineEntries(): Promise { + // 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} Promise resolved when done. + */ + protected sync(): Promise { + 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(); + } +} diff --git a/src/addon/mod/data/data.module.ts b/src/addon/mod/data/data.module.ts new file mode 100644 index 000000000..6231d8e79 --- /dev/null +++ b/src/addon/mod/data/data.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/data.scss b/src/addon/mod/data/data.scss new file mode 100644 index 000000000..75b6d30ae --- /dev/null +++ b/src/addon/mod/data/data.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/src/addon/mod/data/fields/checkbox/checkbox.module.ts b/src/addon/mod/data/fields/checkbox/checkbox.module.ts new file mode 100644 index 000000000..5ac3377f2 --- /dev/null +++ b/src/addon/mod/data/fields/checkbox/checkbox.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/checkbox/component/checkbox.html b/src/addon/mod/data/fields/checkbox/component/checkbox.html new file mode 100644 index 000000000..8006a3fdb --- /dev/null +++ b/src/addon/mod/data/fields/checkbox/component/checkbox.html @@ -0,0 +1,15 @@ + + + + {{option.key}} + + + + + {{ 'addon.mod_data.selectedrequired' | translate }} + + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/checkbox/component/checkbox.ts b/src/addon/mod/data/fields/checkbox/component/checkbox.ts new file mode 100644 index 000000000..5d0ef6f35 --- /dev/null +++ b/src/addon/mod/data/fields/checkbox/component/checkbox.ts @@ -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('
'); + } +} diff --git a/src/addon/mod/data/fields/checkbox/providers/handler.ts b/src/addon/mod/data/fields/checkbox/providers/handler.ts new file mode 100644 index 000000000..48c50d8d7 --- /dev/null +++ b/src/addon/mod/data/fields/checkbox/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/date/component/date.html b/src/addon/mod/data/fields/date/component/date.html new file mode 100644 index 000000000..9d69823cc --- /dev/null +++ b/src/addon/mod/data/fields/date/component/date.html @@ -0,0 +1,14 @@ + + + + + + + {{ 'addon.mod_data.usedate' | translate }} + + + + + + + diff --git a/src/addon/mod/data/fields/date/component/date.ts b/src/addon/mod/data/fields/date/component/date.ts new file mode 100644 index 000000000..c2c368be8 --- /dev/null +++ b/src/addon/mod/data/fields/date/component/date.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/date/date.module.ts b/src/addon/mod/data/fields/date/date.module.ts new file mode 100644 index 000000000..faf696cf5 --- /dev/null +++ b/src/addon/mod/data/fields/date/date.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/date/providers/handler.ts b/src/addon/mod/data/fields/date/providers/handler.ts new file mode 100644 index 000000000..f36d166b7 --- /dev/null +++ b/src/addon/mod/data/fields/date/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/field.module.ts b/src/addon/mod/data/fields/field.module.ts new file mode 100644 index 000000000..3bd3d9e3d --- /dev/null +++ b/src/addon/mod/data/fields/field.module.ts @@ -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 { } diff --git a/src/addon/mod/data/fields/file/component/file.html b/src/addon/mod/data/fields/file/component/file.html new file mode 100644 index 000000000..9b6dd2791 --- /dev/null +++ b/src/addon/mod/data/fields/file/component/file.html @@ -0,0 +1,19 @@ + + + + + + + + + + + +
+ + + + + +
+
diff --git a/src/addon/mod/data/fields/file/component/file.ts b/src/addon/mod/data/fields/file/component/file.ts new file mode 100644 index 000000000..e39faafe5 --- /dev/null +++ b/src/addon/mod/data/fields/file/component/file.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/file/file.module.ts b/src/addon/mod/data/fields/file/file.module.ts new file mode 100644 index 000000000..b2d240329 --- /dev/null +++ b/src/addon/mod/data/fields/file/file.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/file/providers/handler.ts b/src/addon/mod/data/fields/file/providers/handler.ts new file mode 100644 index 000000000..38a58e7de --- /dev/null +++ b/src/addon/mod/data/fields/file/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/latlong/component/latlong.html b/src/addon/mod/data/fields/latlong/component/latlong.html new file mode 100644 index 000000000..a219e554f --- /dev/null +++ b/src/addon/mod/data/fields/latlong/component/latlong.html @@ -0,0 +1,19 @@ + + + + +
+ + °N +
+
+ + °E +
+ +
+ + + + {{ formatLatLong(north, east) }} + \ No newline at end of file diff --git a/src/addon/mod/data/fields/latlong/component/latlong.ts b/src/addon/mod/data/fields/latlong/component/latlong.ts new file mode 100644 index 000000000..2b7fda2be --- /dev/null +++ b/src/addon/mod/data/fields/latlong/component/latlong.ts @@ -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; + } +} diff --git a/src/addon/mod/data/fields/latlong/latlong.module.ts b/src/addon/mod/data/fields/latlong/latlong.module.ts new file mode 100644 index 000000000..5b9b4f11a --- /dev/null +++ b/src/addon/mod/data/fields/latlong/latlong.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/latlong/providers/handler.ts b/src/addon/mod/data/fields/latlong/providers/handler.ts new file mode 100644 index 000000000..fb549c7c4 --- /dev/null +++ b/src/addon/mod/data/fields/latlong/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/menu/component/menu.html b/src/addon/mod/data/fields/menu/component/menu.html new file mode 100644 index 000000000..9a9357500 --- /dev/null +++ b/src/addon/mod/data/fields/menu/component/menu.html @@ -0,0 +1,10 @@ + + + + {{ 'addon.mod_data.menuchoose' | translate }} + {{option}} + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/menu/component/menu.ts b/src/addon/mod/data/fields/menu/component/menu.ts new file mode 100644 index 000000000..e722375ef --- /dev/null +++ b/src/addon/mod/data/fields/menu/component/menu.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/menu/menu.module.ts b/src/addon/mod/data/fields/menu/menu.module.ts new file mode 100644 index 000000000..2c4a16427 --- /dev/null +++ b/src/addon/mod/data/fields/menu/menu.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/menu/providers/handler.ts b/src/addon/mod/data/fields/menu/providers/handler.ts new file mode 100644 index 000000000..e8427cc66 --- /dev/null +++ b/src/addon/mod/data/fields/menu/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/multimenu/component/multimenu.html b/src/addon/mod/data/fields/multimenu/component/multimenu.html new file mode 100644 index 000000000..e1d9081ba --- /dev/null +++ b/src/addon/mod/data/fields/multimenu/component/multimenu.html @@ -0,0 +1,16 @@ + + + + {{option.key}} + + + + + + {{ 'addon.mod_data.selectedrequired' | translate }} + + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/multimenu/component/multimenu.ts b/src/addon/mod/data/fields/multimenu/component/multimenu.ts new file mode 100644 index 000000000..a93ee67d5 --- /dev/null +++ b/src/addon/mod/data/fields/multimenu/component/multimenu.ts @@ -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('
'); + } +} diff --git a/src/addon/mod/data/fields/multimenu/multimenu.module.ts b/src/addon/mod/data/fields/multimenu/multimenu.module.ts new file mode 100644 index 000000000..7b6b72d12 --- /dev/null +++ b/src/addon/mod/data/fields/multimenu/multimenu.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/multimenu/providers/handler.ts b/src/addon/mod/data/fields/multimenu/providers/handler.ts new file mode 100644 index 000000000..716da06bf --- /dev/null +++ b/src/addon/mod/data/fields/multimenu/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/number/component/number.html b/src/addon/mod/data/fields/number/component/number.html new file mode 100644 index 000000000..aa71edc43 --- /dev/null +++ b/src/addon/mod/data/fields/number/component/number.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/addon/mod/data/fields/number/component/number.ts b/src/addon/mod/data/fields/number/component/number.ts new file mode 100644 index 000000000..c7dcb1127 --- /dev/null +++ b/src/addon/mod/data/fields/number/component/number.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/number/number.module.ts b/src/addon/mod/data/fields/number/number.module.ts new file mode 100644 index 000000000..ce2acc789 --- /dev/null +++ b/src/addon/mod/data/fields/number/number.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/number/providers/handler.ts b/src/addon/mod/data/fields/number/providers/handler.ts new file mode 100644 index 000000000..40663e8f0 --- /dev/null +++ b/src/addon/mod/data/fields/number/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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; + } +} diff --git a/src/addon/mod/data/fields/picture/component/picture.html b/src/addon/mod/data/fields/picture/component/picture.html new file mode 100644 index 000000000..ee6cdcad0 --- /dev/null +++ b/src/addon/mod/data/fields/picture/component/picture.html @@ -0,0 +1,16 @@ + + + + + + {{ 'addon.mod_data.alttext' | translate }} + + + + + + + + + + diff --git a/src/addon/mod/data/fields/picture/component/picture.ts b/src/addon/mod/data/fields/picture/component/picture.ts new file mode 100644 index 000000000..5c6796d90 --- /dev/null +++ b/src/addon/mod/data/fields/picture/component/picture.ts @@ -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 || ''; + } + } +} diff --git a/src/addon/mod/data/fields/picture/picture.module.ts b/src/addon/mod/data/fields/picture/picture.module.ts new file mode 100644 index 000000000..c666e1bad --- /dev/null +++ b/src/addon/mod/data/fields/picture/picture.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/picture/providers/handler.ts b/src/addon/mod/data/fields/picture/providers/handler.ts new file mode 100644 index 000000000..a5eab43b0 --- /dev/null +++ b/src/addon/mod/data/fields/picture/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/radiobutton/component/radiobutton.html b/src/addon/mod/data/fields/radiobutton/component/radiobutton.html new file mode 100644 index 000000000..9a9357500 --- /dev/null +++ b/src/addon/mod/data/fields/radiobutton/component/radiobutton.html @@ -0,0 +1,10 @@ + + + + {{ 'addon.mod_data.menuchoose' | translate }} + {{option}} + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts b/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts new file mode 100644 index 000000000..e2b64425b --- /dev/null +++ b/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/radiobutton/providers/handler.ts b/src/addon/mod/data/fields/radiobutton/providers/handler.ts new file mode 100644 index 000000000..6efbd744b --- /dev/null +++ b/src/addon/mod/data/fields/radiobutton/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/radiobutton/radiobutton.module.ts b/src/addon/mod/data/fields/radiobutton/radiobutton.module.ts new file mode 100644 index 000000000..d361bc934 --- /dev/null +++ b/src/addon/mod/data/fields/radiobutton/radiobutton.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/text/component/text.html b/src/addon/mod/data/fields/text/component/text.html new file mode 100644 index 000000000..fa8846daf --- /dev/null +++ b/src/addon/mod/data/fields/text/component/text.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/text/component/text.ts b/src/addon/mod/data/fields/text/component/text.ts new file mode 100644 index 000000000..1d9523d51 --- /dev/null +++ b/src/addon/mod/data/fields/text/component/text.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/text/providers/handler.ts b/src/addon/mod/data/fields/text/providers/handler.ts new file mode 100644 index 000000000..19d49b713 --- /dev/null +++ b/src/addon/mod/data/fields/text/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/text/text.module.ts b/src/addon/mod/data/fields/text/text.module.ts new file mode 100644 index 000000000..a96ddf9de --- /dev/null +++ b/src/addon/mod/data/fields/text/text.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/textarea/component/textarea.html b/src/addon/mod/data/fields/textarea/component/textarea.html new file mode 100644 index 000000000..861d1edc9 --- /dev/null +++ b/src/addon/mod/data/fields/textarea/component/textarea.html @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/textarea/component/textarea.ts b/src/addon/mod/data/fields/textarea/component/textarea.ts new file mode 100644 index 000000000..532d762fa --- /dev/null +++ b/src/addon/mod/data/fields/textarea/component/textarea.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/textarea/providers/handler.ts b/src/addon/mod/data/fields/textarea/providers/handler.ts new file mode 100644 index 000000000..0a5829698 --- /dev/null +++ b/src/addon/mod/data/fields/textarea/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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; + } +} diff --git a/src/addon/mod/data/fields/textarea/textarea.module.ts b/src/addon/mod/data/fields/textarea/textarea.module.ts new file mode 100644 index 000000000..8c63d53f7 --- /dev/null +++ b/src/addon/mod/data/fields/textarea/textarea.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/url/component/url.html b/src/addon/mod/data/fields/url/component/url.html new file mode 100644 index 000000000..f1c018713 --- /dev/null +++ b/src/addon/mod/data/fields/url/component/url.html @@ -0,0 +1,7 @@ + + + + + + +{{field.name}} \ No newline at end of file diff --git a/src/addon/mod/data/fields/url/component/url.ts b/src/addon/mod/data/fields/url/component/url.ts new file mode 100644 index 000000000..f6d2a8450 --- /dev/null +++ b/src/addon/mod/data/fields/url/component/url.ts @@ -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); + } +} diff --git a/src/addon/mod/data/fields/url/providers/handler.ts b/src/addon/mod/data/fields/url/providers/handler.ts new file mode 100644 index 000000000..7e6ea803e --- /dev/null +++ b/src/addon/mod/data/fields/url/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + 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; + } +} diff --git a/src/addon/mod/data/fields/url/url.module.ts b/src/addon/mod/data/fields/url/url.module.ts new file mode 100644 index 000000000..09cd8911e --- /dev/null +++ b/src/addon/mod/data/fields/url/url.module.ts @@ -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); + } +} diff --git a/src/addon/mod/data/lang/en.json b/src/addon/mod/data/lang/en.json new file mode 100644 index 000000000..fdd3f402a --- /dev/null +++ b/src/addon/mod/data/lang/en.json @@ -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." +} \ No newline at end of file diff --git a/src/addon/mod/data/pages/edit/edit.html b/src/addon/mod/data/pages/edit/edit.html new file mode 100644 index 000000000..85b2fbcba --- /dev/null +++ b/src/addon/mod/data/pages/edit/edit.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + +
+ + +
+ +
+
+
+
diff --git a/src/addon/mod/data/pages/edit/edit.module.ts b/src/addon/mod/data/pages/edit/edit.module.ts new file mode 100644 index 000000000..fac3251a6 --- /dev/null +++ b/src/addon/mod/data/pages/edit/edit.module.ts @@ -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 {} diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts new file mode 100644 index 000000000..55830538f --- /dev/null +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -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} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + 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} Resolved when done. + */ + protected fetchEntryData(): Promise { + 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} Resolved when done. + */ + save(): Promise { + 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} Resolved when done. + */ + setGroup(groupId: number): Promise { + 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 = '\ + '; + 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} Resolved when done. + */ + protected returnToEntryList(): Promise { + 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(); + } + } +} diff --git a/src/addon/mod/data/pages/entry/entry.html b/src/addon/mod/data/pages/entry/entry.html new file mode 100644 index 000000000..55db45b17 --- /dev/null +++ b/src/addon/mod/data/pages/entry/entry.html @@ -0,0 +1,54 @@ + + + + + + + + + + + +
+ + {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} +
+ + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + +
+ + + +
+ + + + + + + + + + + + + + + +
+
diff --git a/src/addon/mod/data/pages/entry/entry.module.ts b/src/addon/mod/data/pages/entry/entry.module.ts new file mode 100644 index 000000000..cebf202e2 --- /dev/null +++ b/src/addon/mod/data/pages/entry/entry.module.ts @@ -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 {} diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts new file mode 100644 index 000000000..b43816471 --- /dev/null +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -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} Resolved when done. + */ + protected fetchEntryData(refresh?: boolean): Promise { + 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} Resolved when done. + */ + gotoEntry(entry: number): Promise { + this.entryId = entry; + this.page = null; + this.entryLoaded = false; + + return this.fetchEntryData(); + } + + /** + * Refresh all the data. + * + * @return {Promise} Promise resolved when done. + */ + protected refreshAllData(): Promise { + 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} Promise resolved when done. + */ + refreshDatabase(refresher?: any): Promise { + 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} Resolved when done. + */ + setGroup(groupId: number): Promise { + 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} Resolved when done. + */ + protected setEntryIdFromPage(dataId: number, pageNumber?: number, group?: number): Promise { + 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(); + } +} diff --git a/src/addon/mod/data/pages/index/index.html b/src/addon/mod/data/pages/index/index.html new file mode 100644 index 000000000..ff39e903d --- /dev/null +++ b/src/addon/mod/data/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/data/pages/index/index.module.ts b/src/addon/mod/data/pages/index/index.module.ts new file mode 100644 index 000000000..0215fd3f4 --- /dev/null +++ b/src/addon/mod/data/pages/index/index.module.ts @@ -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 {} diff --git a/src/addon/mod/data/pages/index/index.ts b/src/addon/mod/data/pages/index/index.ts new file mode 100644 index 000000000..f415352a4 --- /dev/null +++ b/src/addon/mod/data/pages/index/index.ts @@ -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; + } +} diff --git a/src/addon/mod/data/pages/search/search.html b/src/addon/mod/data/pages/search/search.html new file mode 100644 index 000000000..687e42605 --- /dev/null +++ b/src/addon/mod/data/pages/search/search.html @@ -0,0 +1,55 @@ + + + {{ 'addon.mod_data.search' | translate }} + + + + + + + +
+ + + + + + {{ 'core.sortby' | translate }} + + + {{field.name}} + + + {{ 'addon.mod_data.timeadded' | translate }} + {{ 'addon.mod_data.timemodified' | translate }} + {{ 'addon.mod_data.authorfirstname' | translate }} + {{ 'addon.mod_data.authorlastname' | translate }} + {{ 'addon.mod_data.approved' | translate }} + + + + + + {{ 'addon.mod_data.ascending' | translate }} + + + + {{ 'addon.mod_data.descending' | translate }} + + + + + + +
+
diff --git a/src/addon/mod/data/pages/search/search.module.ts b/src/addon/mod/data/pages/search/search.module.ts new file mode 100644 index 000000000..09b8cfa41 --- /dev/null +++ b/src/addon/mod/data/pages/search/search.module.ts @@ -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 {} diff --git a/src/addon/mod/data/pages/search/search.ts b/src/addon/mod/data/pages/search/search.ts new file mode 100644 index 000000000..160f0ad59 --- /dev/null +++ b/src/addon/mod/data/pages/search/search.ts @@ -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 = ''; + template = template.replace(replace, render); + }); + + // Not pluginable other search elements. + // Replace firstname field by the text input. + replace = new RegExp('##firstname##', 'gi'); + render = ''; + template = template.replace(replace, render); + + // Replace lastname field by the text input. + replace = new RegExp('##lastname##', 'gi'); + render = ''; + 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); + } +} diff --git a/src/addon/mod/data/providers/approve-link-handler.ts b/src/addon/mod/data/providers/approve-link-handler.ts new file mode 100644 index 000000000..5e7a88a7d --- /dev/null +++ b/src/addon/mod/data/providers/approve-link-handler.ts @@ -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} Resolved with course Id when done. + */ + protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise { + 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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + 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} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + 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); + } +} diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts new file mode 100644 index 000000000..4632f2f36 --- /dev/null +++ b/src/addon/mod/data/providers/data.ts @@ -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} 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 { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a data to be synchronized later. + const storeOffline = (): Promise => { + 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} Promise resolved when the action is done. + */ + addEntryOnline(dataId: number, data: any, groupId?: number, siteId?: string): Promise { + 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} Promise resolved when the action is done. + */ + approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a data to be synchronized later. + const storeOffline = (): Promise => { + 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} Promise resolved when the action is done. + */ + approveEntryOnline(entryId: number, approve: boolean, siteId?: string): Promise { + 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} Promise resolved when the action is done. + */ + deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a data to be synchronized later. + const storeOffline = (): Promise => { + 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} Promise resolved when the action is done. + */ + deleteEntryOnline(entryId: number, siteId?: string): Promise { + 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} Promise resolved when the action is done. + */ + editEntry(dataId: number, entryId: number, courseId: number, contents: any, fields: any, siteId?: string, + forceOffline: boolean = false): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a data to be synchronized later. + const storeOffline = (): Promise => { + 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} Promise resolved when the action is done. + */ + editEntryOnline(entryId: number, data: number, siteId?: string): Promise { + 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} 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 { + 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} 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 { + 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} Promise resolved when the data is retrieved. + */ + protected getDatabaseByKey(courseId: number, key: string, value: any, siteId?: string, forceCache: boolean = false): + Promise { + 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} Promise resolved when the data is retrieved. + */ + getDatabase(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { + 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} Promise resolved when the data is retrieved. + */ + getDatabaseById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise { + 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} Promise resolved when the database is retrieved. + */ + getDatabaseAccessInformation(dataId: number, groupId?: number, offline: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + 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} 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 { + 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} Promise resolved when the database entry is retrieved. + */ + getEntry(dataId: number, entryId: number, siteId?: string): Promise { + 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} Promise resolved when the fields are retrieved. + */ + getFields(dataId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateDatabaseAccessInformationData(dataId: number, siteId?: string): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateEntriesData(dataId: number, siteId?: string): Promise { + 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} Promise resolved when the files are invalidated. + */ + invalidateFiles(moduleId: number, siteId?: string): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateDatabaseData(courseId: number, siteId?: string): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateDatabaseWSData(databaseId: number, siteId: string): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateEntryData(dataId: number, entryId: number, siteId?: string): Promise { + 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} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + * @since 3.3 + */ + isPluginEnabled(siteId?: string): Promise { + 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} Promise resolved when the WS call is successful. + */ + logView(id: number): Promise { + 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} 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 { + 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); + }); + } +} diff --git a/src/addon/mod/data/providers/default-field-handler.ts b/src/addon/mod/data/providers/default-field-handler.ts new file mode 100644 index 000000000..2160d8a9e --- /dev/null +++ b/src/addon/mod/data/providers/default-field-handler.ts @@ -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} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/providers/delete-link-handler.ts b/src/addon/mod/data/providers/delete-link-handler.ts new file mode 100644 index 000000000..9338d7817 --- /dev/null +++ b/src/addon/mod/data/providers/delete-link-handler.ts @@ -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} Resolved with course Id when done. + */ + protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise { + 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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + 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} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + if (typeof params.d == 'undefined' || typeof params.delete == 'undefined') { + // Required fields not defined. Cannot treat the URL. + return false; + } + + return this.dataProvider.isPluginEnabled(siteId); + } +} diff --git a/src/addon/mod/data/providers/edit-link-handler.ts b/src/addon/mod/data/providers/edit-link-handler.ts new file mode 100644 index 000000000..b6857f3ab --- /dev/null +++ b/src/addon/mod/data/providers/edit-link-handler.ts @@ -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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + 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} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + if (typeof params.d == 'undefined') { + // Id not defined. Cannot treat the URL. + return false; + } + + return this.dataProvider.isPluginEnabled(siteId); + } +} diff --git a/src/addon/mod/data/providers/fields-delegate.ts b/src/addon/mod/data/providers/fields-delegate.ts new file mode 100644 index 000000000..43f2895c5 --- /dev/null +++ b/src/addon/mod/data/providers/fields-delegate.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent?(injector: Injector, plugin: any): any | Promise; + + /** + * 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} If the field has changes. + */ + hasFieldDataChanged?(field: any, inputData: any, originalFieldData: any): Promise | 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} Promise resolved with the component to use, undefined if not found. + */ + getComponentForField(injector: Injector, field: any): Promise { + 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} Promise rejected if has changed, resolved if no changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise { + 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]); + } + +} diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts new file mode 100644 index 000000000..dd152ae51 --- /dev/null +++ b/src/addon/mod/data/providers/helper.ts @@ -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 = ''; + 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 = ''; + } + 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} Resolved with an array of entry ID. + */ + getAllEntriesIds(dataId: number, groupId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): + Promise { + 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} That contains object with the answers. + */ + getEditDataFromForm(inputData: any, fields: any, dataId: number, entryId: number, entryContents: any, offline: boolean = false, + siteId?: string): Promise { + 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} That contains object with the files. + */ + getEditTmpFiles(inputData: any, fields: any, dataId: number, entryContents: any): Promise { + 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} Promise resolved with the entry. + */ + getEntry(data: any, entryId: number, offlineActions?: any, siteId?: string): Promise { + 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} 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 { + 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} 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 { + 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} Promise resolved with the files. + */ + getStoredFiles(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise { + 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} True if changed, false if not. + */ + hasEditDataChanged(inputData: any, fields: any, dataId: number, entryContents: any): Promise { + 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} Promise resolved if success, rejected otherwise. + */ + storeFiles(dataId: number, entryId: number, fieldId: number, files: any[], siteId?: string): Promise { + // 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} Promise resolved if success. + */ + uploadOrStoreFiles(dataId: number, itemId: number = 0, entryId: number, fieldId: number, files: any[], offline: boolean, + siteId?: string): Promise { + 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); + } +} diff --git a/src/addon/mod/data/providers/link-handler.ts b/src/addon/mod/data/providers/link-handler.ts new file mode 100644 index 000000000..2f8fd041f --- /dev/null +++ b/src/addon/mod/data/providers/link-handler.ts @@ -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'); + } +} diff --git a/src/addon/mod/data/providers/module-handler.ts b/src/addon/mod/data/providers/module-handler.ts new file mode 100644 index 000000000..b741ec975 --- /dev/null +++ b/src/addon/mod/data/providers/module-handler.ts @@ -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} Whether or not the handler is enabled on a site level. + */ + isEnabled(): Promise { + 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; + } +} diff --git a/src/addon/mod/data/providers/offline.ts b/src/addon/mod/data/providers/offline.ts new file mode 100644 index 000000000..c30316b1a --- /dev/null +++ b/src/addon/mod/data/providers/offline.ts @@ -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} Promise resolved if deleted, rejected if failure. + */ + deleteAllEntryActions(dataId: number, entryId: number, siteId?: string): Promise { + 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} Promise resolved if deleted, rejected if failure. + */ + deleteEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise { + 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} Promise resolved with entries. + */ + getAllEntries(siteId?: string): Promise { + 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} Promise resolved with entries. + */ + getDatabaseEntries(dataId: number, siteId?: string): Promise { + 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} Promise resolved with entry. + */ + getEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise { + 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} Promise resolved with entry actions. + */ + getEntryActions(dataId: number, entryId: number, siteId?: string): Promise { + 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} Promise resolved with boolean: true if has offline answers, false otherwise. + */ + hasOfflineData(dataId: number, siteId?: string): Promise { + 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} Promise resolved with the path. + */ + protected getDatabaseFolder(dataId: number, siteId?: string): Promise { + 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} Promise resolved with the path. + */ + getEntryFieldFolder(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise { + 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} 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 { + + 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); + }); + } + +} diff --git a/src/addon/mod/data/providers/prefetch-handler.ts b/src/addon/mod/data/providers/prefetch-handler.ts new file mode 100644 index 000000000..81413449f --- /dev/null +++ b/src/addon/mod/data/providers/prefetch-handler.ts @@ -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} Promise resolved when all content is downloaded. Data returned is not reliable. + */ + downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise { + 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} All unique entries. + */ + protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + 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} Promise resolved with the info fetched. + */ + protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + 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} Promise resolved with the list of files. + */ + getFiles(module: any, courseId: number, single?: boolean): Promise { + 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} Promise resolved with list of intro files. + */ + getIntroFiles(module: any, courseId: number): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + 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} Promise resolved when invalidated. + */ + invalidateModule(module: any, courseId: number): Promise { + 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} Promise resolved with true if downloadable, resolved with false otherwise. + */ + isDownloadable(module: any, courseId: number): boolean | Promise { + 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} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return this.dataProvider.isPluginEnabled(); + } +} diff --git a/src/addon/mod/data/providers/show-link-handler.ts b/src/addon/mod/data/providers/show-link-handler.ts new file mode 100644 index 000000000..1d30f33ea --- /dev/null +++ b/src/addon/mod/data/providers/show-link-handler.ts @@ -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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + 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} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + 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); + } +} diff --git a/src/addon/mod/data/providers/sync-cron-handler.ts b/src/addon/mod/data/providers/sync-cron-handler.ts new file mode 100644 index 000000000..2560e571b --- /dev/null +++ b/src/addon/mod/data/providers/sync-cron-handler.ts @@ -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} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + 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. + } +} diff --git a/src/addon/mod/data/providers/sync.ts b/src/addon/mod/data/providers/sync.ts new file mode 100644 index 000000000..e29f29ab6 --- /dev/null +++ b/src/addon/mod/data/providers/sync.ts @@ -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} Promise resolved with boolean: true if has data to sync, false otherwise. + */ + hasDataToSync(dataId: number, siteId?: string): Promise { + 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} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllDatabases(siteId?: string): Promise { + 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} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllDatabasesFunc(siteId?: string): Promise { + // 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} Promise resolved when the data is synced or if it doesn't need to be synced. + */ + syncDatabaseIfNeeded(dataId: number, siteId?: string): Promise { + 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} Promise resolved if sync is successful, rejected otherwise. + */ + syncDatabase(dataId: number, siteId?: string): Promise { + 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} Promise resolved if success, rejected otherwise. + */ + protected syncEntry(data: any, entryActions: any[], result: any, siteId?: string): Promise { + 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); + }); + } + +} diff --git a/src/addon/mod/feedback/providers/helper.ts b/src/addon/mod/feedback/providers/helper.ts index 7f5527623..cbd1ea4d5 100644 --- a/src/addon/mod/feedback/providers/helper.ts +++ b/src/addon/mod/feedback/providers/helper.ts @@ -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; } diff --git a/src/addon/mod/feedback/providers/link-handler.ts b/src/addon/mod/feedback/providers/link-handler.ts index e4df7edf0..fe080bf63 100644 --- a/src/addon/mod/feedback/providers/link-handler.ts +++ b/src/addon/mod/feedback/providers/link-handler.ts @@ -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'); } } diff --git a/src/addon/mod/feedback/providers/prefetch-handler.ts b/src/addon/mod/feedback/providers/prefetch-handler.ts index b5802f326..a3200300f 100644 --- a/src/addon/mod/feedback/providers/prefetch-handler.ts +++ b/src/addon/mod/feedback/providers/prefetch-handler.ts @@ -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} Promise resolved with the list of files. */ getFiles(module: any, courseId: number, single?: boolean): Promise { diff --git a/src/addon/mod/feedback/providers/sync.ts b/src/addon/mod/feedback/providers/sync.ts index 7653014b8..469090738 100644 --- a/src/addon/mod/feedback/providers/sync.ts +++ b/src/addon/mod/feedback/providers/sync.ts @@ -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(() => { diff --git a/src/addon/userprofilefield/datetime/component/datetime.html b/src/addon/userprofilefield/datetime/component/datetime.html index 57b8e7ca2..5f7a554a0 100644 --- a/src/addon/userprofilefield/datetime/component/datetime.html +++ b/src/addon/userprofilefield/datetime/component/datetime.html @@ -6,5 +6,5 @@ {{ field.name }} - + \ No newline at end of file diff --git a/src/addon/userprofilefield/menu/component/menu.html b/src/addon/userprofilefield/menu/component/menu.html index 39f54dbc8..ce81fe385 100644 --- a/src/addon/userprofilefield/menu/component/menu.html +++ b/src/addon/userprofilefield/menu/component/menu.html @@ -6,7 +6,7 @@ {{ field.name }} - + {{ 'core.choosedots' | translate }} {{option}} diff --git a/src/addon/userprofilefield/text/component/text.html b/src/addon/userprofilefield/text/component/text.html index aaa43b1ac..8701248f2 100644 --- a/src/addon/userprofilefield/text/component/text.html +++ b/src/addon/userprofilefield/text/component/text.html @@ -6,5 +6,5 @@ {{ field.name }} - + diff --git a/src/app/app.module.ts b/src/app/app.module.ts index caf403c06..3d2eee425 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/app.scss b/src/app/app.scss index 510ee0f62..b7a30bbb2 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -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 { diff --git a/src/classes/delegate.ts b/src/classes/delegate.ts index 412dad239..b95626153 100644 --- a/src/classes/delegate.ts +++ b/src/classes/delegate.ts @@ -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). * diff --git a/src/components/dynamic-component/dynamic-component.ts b/src/components/dynamic-component/dynamic-component.ts index 1692b7663..5e232ce04 100644 --- a/src/components/dynamic-component/dynamic-component.ts +++ b/src/components/dynamic-component/dynamic-component.ts @@ -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; - } } diff --git a/src/components/input-errors/input-errors.html b/src/components/input-errors/input-errors.html index 51be581b5..390ad0ce0 100644 --- a/src/components/input-errors/input-errors.html +++ b/src/components/input-errors/input-errors.html @@ -1,5 +1,8 @@ -