From cf927d4344c9768d77faab6f8091228a30d9f46c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 13 Apr 2018 15:32:06 +0200 Subject: [PATCH] MOBILE-2334 assign: Implement submission plugins --- src/addon/mod/assign/assign.module.ts | 4 + .../classes/submission-plugin-component.ts | 40 ++ .../assign/components/components.module.ts | 7 +- .../submission-plugin/submission-plugin.html | 16 + .../submission-plugin/submission-plugin.ts | 98 +++++ .../components/submission/submission.html | 2 +- .../components/submission/submission.ts | 11 +- .../submission/comments/comments.module.ts | 48 +++ .../comments/component/comments.html | 4 + .../submission/comments/component/comments.ts | 50 +++ .../assign/submission/comments/lang/en.json | 3 + .../submission/comments/providers/handler.ts | 93 +++++ .../submission/file/component/file.html | 17 + .../assign/submission/file/component/file.ts | 74 ++++ .../mod/assign/submission/file/file.module.ts | 50 +++ .../mod/assign/submission/file/lang/en.json | 3 + .../submission/file/providers/handler.ts | 361 ++++++++++++++++++ .../onlinetext/component/onlinetext.html | 21 + .../onlinetext/component/onlinetext.ts | 129 +++++++ .../assign/submission/onlinetext/lang/en.json | 3 + .../onlinetext/onlinetext.module.ts | 50 +++ .../onlinetext/providers/handler.ts | 277 ++++++++++++++ .../assign/submission/submission.module.ts | 31 ++ src/app/app.scss | 6 + src/components/attachments/attachments.html | 25 ++ src/components/attachments/attachments.ts | 135 +++++++ src/components/components.module.ts | 7 +- src/components/file/file.ts | 4 +- src/components/local-file/local-file.ts | 4 +- .../fileuploader/providers/fileuploader.ts | 2 +- 30 files changed, 1564 insertions(+), 11 deletions(-) create mode 100644 src/addon/mod/assign/classes/submission-plugin-component.ts create mode 100644 src/addon/mod/assign/components/submission-plugin/submission-plugin.html create mode 100644 src/addon/mod/assign/components/submission-plugin/submission-plugin.ts create mode 100644 src/addon/mod/assign/submission/comments/comments.module.ts create mode 100644 src/addon/mod/assign/submission/comments/component/comments.html create mode 100644 src/addon/mod/assign/submission/comments/component/comments.ts create mode 100644 src/addon/mod/assign/submission/comments/lang/en.json create mode 100644 src/addon/mod/assign/submission/comments/providers/handler.ts create mode 100644 src/addon/mod/assign/submission/file/component/file.html create mode 100644 src/addon/mod/assign/submission/file/component/file.ts create mode 100644 src/addon/mod/assign/submission/file/file.module.ts create mode 100644 src/addon/mod/assign/submission/file/lang/en.json create mode 100644 src/addon/mod/assign/submission/file/providers/handler.ts create mode 100644 src/addon/mod/assign/submission/onlinetext/component/onlinetext.html create mode 100644 src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts create mode 100644 src/addon/mod/assign/submission/onlinetext/lang/en.json create mode 100644 src/addon/mod/assign/submission/onlinetext/onlinetext.module.ts create mode 100644 src/addon/mod/assign/submission/onlinetext/providers/handler.ts create mode 100644 src/addon/mod/assign/submission/submission.module.ts create mode 100644 src/components/attachments/attachments.html create mode 100644 src/components/attachments/attachments.ts diff --git a/src/addon/mod/assign/assign.module.ts b/src/addon/mod/assign/assign.module.ts index cb3d922fb..d623ec3fb 100644 --- a/src/addon/mod/assign/assign.module.ts +++ b/src/addon/mod/assign/assign.module.ts @@ -27,10 +27,14 @@ import { AddonModAssignDefaultSubmissionHandler } from './providers/default-subm import { AddonModAssignModuleHandler } from './providers/module-handler'; import { AddonModAssignPrefetchHandler } from './providers/prefetch-handler'; import { AddonModAssignSyncCronHandler } from './providers/sync-cron-handler'; +import { AddonModAssignSubmissionModule } from './submission/submission.module'; @NgModule({ declarations: [ ], + imports: [ + AddonModAssignSubmissionModule + ], providers: [ AddonModAssignProvider, AddonModAssignOfflineProvider, diff --git a/src/addon/mod/assign/classes/submission-plugin-component.ts b/src/addon/mod/assign/classes/submission-plugin-component.ts new file mode 100644 index 000000000..35e750585 --- /dev/null +++ b/src/addon/mod/assign/classes/submission-plugin-component.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Input } from '@angular/core'; + +/** + * Base class for component to render a submission plugin. + */ +export class AddonModAssignSubmissionPluginComponent { + @Input() assign: any; // The assignment. + @Input() submission: any; // The submission. + @Input() plugin: any; // The plugin object. + @Input() configs: any; // The configs for the plugin. + @Input() edit: boolean; // Whether the user is editing. + @Input() allowOffline: boolean; // Whether to allow offline. + + constructor() { + // Nothing to do. + } + + /** + * Invalidate the data. + * + * @return {Promise} Promise resolved when done. + */ + invalidate(): Promise { + return Promise.resolve(); + } +} diff --git a/src/addon/mod/assign/components/components.module.ts b/src/addon/mod/assign/components/components.module.ts index e45d5d817..94e81c86e 100644 --- a/src/addon/mod/assign/components/components.module.ts +++ b/src/addon/mod/assign/components/components.module.ts @@ -22,11 +22,13 @@ import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { AddonModAssignIndexComponent } from './index/index'; import { AddonModAssignSubmissionComponent } from './submission/submission'; +import { AddonModAssignSubmissionPluginComponent } from './submission-plugin/submission-plugin'; @NgModule({ declarations: [ AddonModAssignIndexComponent, - AddonModAssignSubmissionComponent + AddonModAssignSubmissionComponent, + AddonModAssignSubmissionPluginComponent ], imports: [ CommonModule, @@ -41,7 +43,8 @@ import { AddonModAssignSubmissionComponent } from './submission/submission'; ], exports: [ AddonModAssignIndexComponent, - AddonModAssignSubmissionComponent + AddonModAssignSubmissionComponent, + AddonModAssignSubmissionPluginComponent ], entryComponents: [ AddonModAssignIndexComponent diff --git a/src/addon/mod/assign/components/submission-plugin/submission-plugin.html b/src/addon/mod/assign/components/submission-plugin/submission-plugin.html new file mode 100644 index 000000000..0a82623c2 --- /dev/null +++ b/src/addon/mod/assign/components/submission-plugin/submission-plugin.html @@ -0,0 +1,16 @@ + + + + + +

{{ plugin.name }}

+ + {{ 'addon.mod_assign.submissionnotsupported' | translate }} + +

+ +

+ +
+
+
diff --git a/src/addon/mod/assign/components/submission-plugin/submission-plugin.ts b/src/addon/mod/assign/components/submission-plugin/submission-plugin.ts new file mode 100644 index 000000000..b2f186a00 --- /dev/null +++ b/src/addon/mod/assign/components/submission-plugin/submission-plugin.ts @@ -0,0 +1,98 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, Input, OnInit, Injector, ViewChild } from '@angular/core'; +import { AddonModAssignProvider } from '../../providers/assign'; +import { AddonModAssignHelperProvider } from '../../providers/helper'; +import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate'; +import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; + +/** + * Component that displays an assignment submission plugin. + */ +@Component({ + selector: 'addon-mod-assign-submission-plugin', + templateUrl: 'submission-plugin.html', +}) +export class AddonModAssignSubmissionPluginComponent implements OnInit { + @ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent; + + @Input() assign: any; // The assignment. + @Input() submission: any; // The submission. + @Input() plugin: any; // The plugin object. + @Input() edit: boolean | string; // Whether the user is editing. + @Input() allowOffline: boolean | string; // Whether to allow offline. + + pluginComponent: any; // Component to render the plugin. + data: any; // Data to pass to the component. + + // Data to render the plugin if it isn't supported. + component = AddonModAssignProvider.COMPONENT; + text = ''; + files = []; + notSupported: boolean; + pluginLoaded: boolean; + + constructor(protected injector: Injector, protected submissionDelegate: AddonModAssignSubmissionDelegate, + protected assignProvider: AddonModAssignProvider, protected assignHelper: AddonModAssignHelperProvider) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + if (!this.plugin) { + return; + } + + this.plugin.name = this.submissionDelegate.getPluginName(this.plugin); + if (!this.plugin.name) { + return; + } + + this.edit = this.edit && this.edit !== 'false'; + this.allowOffline = this.allowOffline && this.allowOffline !== 'false'; + + // Check if the plugin has defined its own component to render itself. + this.submissionDelegate.getComponentForPlugin(this.injector, this.plugin, this.edit).then((component) => { + this.pluginComponent = component; + + if (component) { + // Prepare the data to pass to the component. + this.data = { + assign: this.assign, + submission: this.submission, + plugin: this.plugin, + configs: this.assignHelper.getPluginConfig(this.assign, 'assignsubmission', this.plugin.type), + edit: this.edit, + allowOffline: this.allowOffline + }; + } else { + // Data to render the plugin. + this.text = this.assignProvider.getSubmissionPluginText(this.plugin); + this.files = this.assignProvider.getSubmissionPluginAttachments(this.plugin); + this.notSupported = this.submissionDelegate.isPluginSupported(this.plugin.type); + this.pluginLoaded = true; + } + }); + } + + /** + * Invalidate the plugin data. + * + * @return {Promise} Promise resolved when done. + */ + invalidate(): Promise { + return Promise.resolve(this.dynamicComponent && this.dynamicComponent.callComponentFunction('invalidate', [])); + } +} diff --git a/src/addon/mod/assign/components/submission/submission.html b/src/addon/mod/assign/components/submission/submission.html index 81dbcf04a..ab02ee679 100644 --- a/src/addon/mod/assign/components/submission/submission.html +++ b/src/addon/mod/assign/components/submission/submission.html @@ -27,7 +27,7 @@ - + diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts index 9383ea511..978c2b391 100644 --- a/src/addon/mod/assign/components/submission/submission.ts +++ b/src/addon/mod/assign/components/submission/submission.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, OnDestroy, ViewChild, Optional } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy, ViewChild, Optional, ViewChildren, QueryList } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; @@ -35,6 +35,7 @@ import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; import * as moment from 'moment'; import { CoreTabsComponent } from '@components/tabs/tabs'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; /** * Component that displays an assignment submission. @@ -45,6 +46,7 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view'; }) export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { @ViewChild(CoreTabsComponent) tabs: CoreTabsComponent; + @ViewChildren(AddonModAssignSubmissionPluginComponent) submissionComponents: QueryList; @Input() courseId: number; // Course ID the submission belongs to. @Input() moduleId: number; // Module ID the submission belongs to. @@ -328,6 +330,13 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { promises.push(this.gradesHelper.invalidateGradeModuleItems(this.courseId, this.submitId)); promises.push(this.courseProvider.invalidateModule(this.moduleId)); + // Invalidate plugins. + if (this.submissionComponents && this.submissionComponents.length) { + this.submissionComponents.forEach((component) => { + promises.push(component.invalidate()); + }); + } + return Promise.all(promises).catch(() => { // Ignore errors. }).then(() => { diff --git a/src/addon/mod/assign/submission/comments/comments.module.ts b/src/addon/mod/assign/submission/comments/comments.module.ts new file mode 100644 index 000000000..06ea8bafd --- /dev/null +++ b/src/addon/mod/assign/submission/comments/comments.module.ts @@ -0,0 +1,48 @@ +// (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 { AddonModAssignSubmissionCommentsHandler } from './providers/handler'; +import { AddonModAssignSubmissionCommentsComponent } from './component/comments'; +import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate'; +import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; + +@NgModule({ + declarations: [ + AddonModAssignSubmissionCommentsComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreCommentsComponentsModule + ], + providers: [ + AddonModAssignSubmissionCommentsHandler + ], + exports: [ + AddonModAssignSubmissionCommentsComponent + ], + entryComponents: [ + AddonModAssignSubmissionCommentsComponent + ] +}) +export class AddonModAssignSubmissionCommentsModule { + constructor(submissionDelegate: AddonModAssignSubmissionDelegate, handler: AddonModAssignSubmissionCommentsHandler) { + submissionDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/assign/submission/comments/component/comments.html b/src/addon/mod/assign/submission/comments/component/comments.html new file mode 100644 index 000000000..e259a78fc --- /dev/null +++ b/src/addon/mod/assign/submission/comments/component/comments.html @@ -0,0 +1,4 @@ + +

{{plugin.name}}

+ +
diff --git a/src/addon/mod/assign/submission/comments/component/comments.ts b/src/addon/mod/assign/submission/comments/component/comments.ts new file mode 100644 index 000000000..c27751bcc --- /dev/null +++ b/src/addon/mod/assign/submission/comments/component/comments.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 { CoreCommentsProvider } from '@core/comments/providers/comments'; +import { CoreCommentsCommentsComponent } from '@core/comments/components/comments/comments'; +import { AddonModAssignSubmissionPluginComponent } from '../../../classes/submission-plugin-component'; + +/** + * Component to render a comments submission plugin. + */ +@Component({ + selector: 'addon-mod-assign-submission-comments', + templateUrl: 'comments.html' +}) +export class AddonModAssignSubmissionCommentsComponent extends AddonModAssignSubmissionPluginComponent { + @ViewChild(CoreCommentsCommentsComponent) commentsComponent: CoreCommentsCommentsComponent; + + constructor(protected commentsProvider: CoreCommentsProvider) { + super(); + } + + /** + * Invalidate the data. + * + * @return {Promise} Promise resolved when done. + */ + invalidate(): Promise { + return this.commentsProvider.invalidateCommentsData('module', this.assign.cmid, 'assignsubmission_comments', + this.submission.id, 'submission_comments'); + } + + /** + * Show the comments. + */ + showComments(): void { + this.commentsComponent && this.commentsComponent.openComments(); + } +} diff --git a/src/addon/mod/assign/submission/comments/lang/en.json b/src/addon/mod/assign/submission/comments/lang/en.json new file mode 100644 index 000000000..c69c732aa --- /dev/null +++ b/src/addon/mod/assign/submission/comments/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Submission comments" +} \ No newline at end of file diff --git a/src/addon/mod/assign/submission/comments/providers/handler.ts b/src/addon/mod/assign/submission/comments/providers/handler.ts new file mode 100644 index 000000000..fa4f7af2b --- /dev/null +++ b/src/addon/mod/assign/submission/comments/providers/handler.ts @@ -0,0 +1,93 @@ + +// (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 { CoreCommentsProvider } from '@core/comments/providers/comments'; +import { AddonModAssignSubmissionHandler } from '../../../providers/submission-delegate'; +import { AddonModAssignSubmissionCommentsComponent } from '../component/comments'; + +/** + * Handler for comments submission plugin. + */ +@Injectable() +export class AddonModAssignSubmissionCommentsHandler implements AddonModAssignSubmissionHandler { + name = 'AddonModAssignSubmissionCommentsHandler'; + type = 'comments'; + + constructor(private commentsProvider: CoreCommentsProvider) { } + + /** + * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the + * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit + * unfiltered data. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @return {boolean|Promise} Boolean or promise resolved with boolean: whether it can be edited in offline. + */ + canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise { + // This plugin is read only, but return true to prevent blocking the edition. + return true; + } + + /** + * Return the Component to use to display the plugin data, either in read or in edit mode. + * 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} plugin The plugin object. + * @param {boolean} [edit] Whether the user is editing. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + return edit ? undefined : AddonModAssignSubmissionCommentsComponent; + } + + /** + * 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; + } + + /** + * Whether or not the handler is enabled for edit on a site level. + * + * @return {boolean|Promise} Whether or not the handler is enabled for edit on a site level. + */ + isEnabledForEdit(): boolean | Promise { + return true; + } + + /** + * Prefetch any required data for the plugin. + * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise { + return this.commentsProvider.getComments('module', assign.cmid, 'assignsubmission_comments', submission.id, + 'submission_comments', 0, siteId).catch(() => { + // Fail silently (Moodle < 3.1.1, 3.2) + }); + } +} diff --git a/src/addon/mod/assign/submission/file/component/file.html b/src/addon/mod/assign/submission/file/component/file.html new file mode 100644 index 000000000..5264ff0bc --- /dev/null +++ b/src/addon/mod/assign/submission/file/component/file.html @@ -0,0 +1,17 @@ + + +

{{plugin.name}}

+
+ + + + + +
+
+ + +
+ {{plugin.name}} + +
diff --git a/src/addon/mod/assign/submission/file/component/file.ts b/src/addon/mod/assign/submission/file/component/file.ts new file mode 100644 index 000000000..a25ac1bbb --- /dev/null +++ b/src/addon/mod/assign/submission/file/component/file.ts @@ -0,0 +1,74 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { CoreFileSessionProvider } from '@providers/file-session'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { AddonModAssignProvider } from '../../../providers/assign'; +import { AddonModAssignHelperProvider } from '../../../providers/helper'; +import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline'; +import { AddonModAssignSubmissionFileHandler } from '../providers/handler'; +import { AddonModAssignSubmissionPluginComponent } from '../../../classes/submission-plugin-component'; + +/** + * Component to render a file submission plugin. + */ +@Component({ + selector: 'addon-mod-assign-submission-file', + templateUrl: 'file.html' +}) +export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmissionPluginComponent implements OnInit { + + component = AddonModAssignProvider.COMPONENT; + files: any[]; + + constructor(protected fileSessionprovider: CoreFileSessionProvider, protected assignProvider: AddonModAssignProvider, + protected assignOfflineProvider: AddonModAssignOfflineProvider, protected assignHelper: AddonModAssignHelperProvider, + protected fileUploaderProvider: CoreFileUploaderProvider) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Get the offline data. + this.assignOfflineProvider.getSubmission(this.assign.id).catch(() => { + // Error getting data, assume there's no offline submission. + }).then((offlineData) => { + if (offlineData && offlineData.plugindata && offlineData.plugindata.files_filemanager) { + // It has offline data. + let promise; + if (offlineData.plugindata.files_filemanager.offline) { + promise = this.assignHelper.getStoredSubmissionFiles(this.assign.id, + AddonModAssignSubmissionFileHandler.FOLDER_NAME); + } else { + promise = Promise.resolve([]); + } + + return promise.then((offlineFiles) => { + const onlineFiles = offlineData.plugindata.files_filemanager.online || []; + offlineFiles = this.fileUploaderProvider.markOfflineFiles(offlineFiles); + + this.files = onlineFiles.concat(offlineFiles); + }); + } else { + // No offline data, get the online files. + this.files = this.assignProvider.getSubmissionPluginAttachments(this.plugin); + } + }).finally(() => { + this.fileSessionprovider.setFiles(this.component, this.assign.id, this.files); + }); + } +} diff --git a/src/addon/mod/assign/submission/file/file.module.ts b/src/addon/mod/assign/submission/file/file.module.ts new file mode 100644 index 000000000..64c898ba8 --- /dev/null +++ b/src/addon/mod/assign/submission/file/file.module.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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModAssignSubmissionFileHandler } from './providers/handler'; +import { AddonModAssignSubmissionFileComponent } from './component/file'; +import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModAssignSubmissionFileComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModAssignSubmissionFileHandler + ], + exports: [ + AddonModAssignSubmissionFileComponent + ], + entryComponents: [ + AddonModAssignSubmissionFileComponent + ] +}) +export class AddonModAssignSubmissionFileModule { + constructor(submissionDelegate: AddonModAssignSubmissionDelegate, handler: AddonModAssignSubmissionFileHandler) { + submissionDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/assign/submission/file/lang/en.json b/src/addon/mod/assign/submission/file/lang/en.json new file mode 100644 index 000000000..7262ba217 --- /dev/null +++ b/src/addon/mod/assign/submission/file/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "File submissions" +} \ No newline at end of file diff --git a/src/addon/mod/assign/submission/file/providers/handler.ts b/src/addon/mod/assign/submission/file/providers/handler.ts new file mode 100644 index 000000000..1e013db7a --- /dev/null +++ b/src/addon/mod/assign/submission/file/providers/handler.ts @@ -0,0 +1,361 @@ + +// (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 { CoreFileProvider } from '@providers/file'; +import { CoreFileSessionProvider } from '@providers/file-session'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreWSProvider } from '@providers/ws'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { AddonModAssignProvider } from '../../../providers/assign'; +import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline'; +import { AddonModAssignHelperProvider } from '../../../providers/helper'; +import { AddonModAssignSubmissionHandler } from '../../../providers/submission-delegate'; +import { AddonModAssignSubmissionFileComponent } from '../component/file'; + +/** + * Handler for file submission plugin. + */ +@Injectable() +export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmissionHandler { + static FOLDER_NAME = 'submission_file'; + + name = 'AddonModAssignSubmissionFileHandler'; + type = 'file'; + + constructor(private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, + private assignProvider: AddonModAssignProvider, private assignOfflineProvider: AddonModAssignOfflineProvider, + private assignHelper: AddonModAssignHelperProvider, private fileSessionProvider: CoreFileSessionProvider, + private fileUploaderProvider: CoreFileUploaderProvider, private filepoolProvider: CoreFilepoolProvider, + private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider) { } + + /** + * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the + * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit + * unfiltered data. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @return {boolean|Promise} Boolean or promise resolved with boolean: whether it can be edited in offline. + */ + canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise { + // This plugin doesn't use Moodle filters, it can be edited in offline. + return true; + } + + /** + * Should clear temporary data for a cancelled submission. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + */ + clearTmpData(assign: any, submission: any, plugin: any, inputData: any): void { + const files = this.fileSessionProvider.getFiles(AddonModAssignProvider.COMPONENT, assign.id); + + // Clear the files in session for this assign. + this.fileSessionProvider.clearFiles(AddonModAssignProvider.COMPONENT, assign.id); + + // Now delete the local files from the tmp folder. + this.fileUploaderProvider.clearTmpFiles(files); + } + + /** + * This function will be called when the user wants to create a new submission based on the previous one. + * It should add to pluginData the data to send to server based in the data in plugin (previous attempt). + * + * @param {any} assign The assignment. + * @param {any} plugin The plugin object. + * @param {any} pluginData Object where to store the data to send. + * @param {number} [userId] User ID. If not defined, site's current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + copySubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise { + // We need to re-upload all the existing files. + const files = this.assignProvider.getSubmissionPluginAttachments(plugin); + + return this.assignHelper.uploadFiles(assign.id, files).then((itemId) => { + pluginData.files_filemanager = itemId; + }); + } + + /** + * Return the Component to use to display the plugin data, either in read or in edit mode. + * 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} plugin The plugin object. + * @param {boolean} [edit] Whether the user is editing. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + return AddonModAssignSubmissionFileComponent; + } + + /** + * Delete any stored data for the plugin and submission. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} offlineData Offline data stored. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + deleteOfflineData(assign: any, submission: any, plugin: any, offlineData: any, siteId?: string): void | Promise { + return this.assignHelper.deleteStoredSubmissionFiles(assign.id, AddonModAssignSubmissionFileHandler.FOLDER_NAME, + submission.userid, siteId).catch(() => { + // Ignore errors, maybe the folder doesn't exist. + }); + } + + /** + * Get files used by this plugin. + * The files returned by this function will be prefetched when the user prefetches the assign. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {any[]|Promise} The files (or promise resolved with the files). + */ + getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise { + return this.assignProvider.getSubmissionPluginAttachments(plugin); + } + + /** + * Get the size of data (in bytes) this plugin will send to copy a previous submission. + * + * @param {any} assign The assignment. + * @param {any} plugin The plugin object. + * @return {number|Promise} The size (or promise resolved with size). + */ + getSizeForCopy(assign: any, plugin: any): number | Promise { + const files = this.assignProvider.getSubmissionPluginAttachments(plugin), + promises = []; + let totalSize = 0; + + files.forEach((file) => { + promises.push(this.wsProvider.getRemoteFileSize(file.fileurl).then((size) => { + if (size == -1) { + // Couldn't determine the size, reject. + return Promise.reject(null); + } + + totalSize += size; + })); + }); + + return Promise.all(promises).then(() => { + return totalSize; + }); + } + + /** + * Get the size of data (in bytes) this plugin will send to add or edit a submission. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + * @return {number|Promise} The size (or promise resolved with size). + */ + getSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): number | Promise { + const siteId = this.sitesProvider.getCurrentSiteId(); + + // Check if there's any change. + if (this.hasDataChanged(assign, submission, plugin, inputData)) { + const files = this.fileSessionProvider.getFiles(AddonModAssignProvider.COMPONENT, assign.id), + promises = []; + let totalSize = 0; + + files.forEach((file) => { + if (file.filename && !file.name) { + // It's a remote file. First check if we have the file downloaded since it's more reliable. + promises.push(this.filepoolProvider.getFilePathByUrl(siteId, file.fileurl).then((path) => { + return this.fileProvider.getFile(path).then((fileEntry) => { + return this.fileProvider.getFileObjectFromFileEntry(fileEntry); + }).then((file) => { + totalSize += file.size; + }); + }).catch(() => { + // Error getting the file, maybe it's not downloaded. Get remote size. + return this.wsProvider.getRemoteFileSize(file.fileurl).then((size) => { + if (size == -1) { + // Couldn't determine the size, reject. + return Promise.reject(null); + } + + totalSize += size; + }); + })); + } else if (file.name) { + // It's a local file, get its size. + promises.push(this.fileProvider.getFileObjectFromFileEntry(file).then((file) => { + totalSize += file.size; + })); + } + }); + + return Promise.all(promises).then(() => { + return totalSize; + }); + } else { + // Nothing has changed, we won't upload any file. + return 0; + } + } + + /** + * Check if the submission data has changed for this plugin. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + * @return {boolean|Promise} Boolean (or promise resolved with boolean): whether the data has changed. + */ + hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise { + // Check if there's any offline data. + return this.assignOfflineProvider.getSubmission(assign.id, submission.userid).catch(() => { + // No offline data found. + }).then((offlineData) => { + if (offlineData && offlineData.plugindata && offlineData.plugindata.files_filemanager) { + // Has offline data, return the number of files. + return offlineData.plugindata.files_filemanager.offline + offlineData.plugindata.files_filemanager.online.length; + } + + // No offline data, return the number of online files. + const pluginFiles = this.assignProvider.getSubmissionPluginAttachments(plugin); + + return pluginFiles && pluginFiles.length; + }).then((numFiles) => { + const currentFiles = this.fileSessionProvider.getFiles(AddonModAssignProvider.COMPONENT, assign.id); + + if (currentFiles.length != numFiles) { + // Number of files has changed. + return true; + } + + // Search if there is any local file added. + for (let i = 0; i < currentFiles.length; i++) { + const file = currentFiles[i]; + if (!file.filename && typeof file.name != 'undefined' && !file.offline) { + // There's a local file added, list has changed. + return true; + } + } + + // No local files and list length is the same, this means the list hasn't changed. + return false; + }); + } + + /** + * 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; + } + + /** + * Whether or not the handler is enabled for edit on a site level. + * + * @return {boolean|Promise} Whether or not the handler is enabled for edit on a site level. + */ + isEnabledForEdit(): boolean | Promise { + return true; + } + + /** + * Prepare and add to pluginData the data to send to the server based on the input data. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + * @param {any} pluginData Object where to store the data to send. + * @param {boolean} [offline] Whether the user is editing in offline. + * @param {number} [userId] User ID. If not defined, site's current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + prepareSubmissionData(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean, + userId?: number, siteId?: string): void | Promise { + + if (this.hasDataChanged(assign, submission, plugin, inputData)) { + // Data has changed, we need to upload new files and re-upload all the existing files. + const currentFiles = this.fileSessionProvider.getFiles(AddonModAssignProvider.COMPONENT, assign.id), + error = this.utils.hasRepeatedFilenames(currentFiles); + + if (error) { + return Promise.reject(error); + } + + return this.assignHelper.uploadOrStoreFiles(assign.id, AddonModAssignSubmissionFileHandler.FOLDER_NAME, + currentFiles, offline, userId, siteId).then((result) => { + pluginData.files_filemanager = result; + }); + } + } + + /** + * Prepare and add to pluginData the data to send to the server based on the offline data stored. + * This will be used when performing a synchronization. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} offlineData Offline data stored. + * @param {any} pluginData Object where to store the data to send. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + prepareSyncData(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string) + : void | Promise { + + const filesData = offlineData && offlineData.plugindata && offlineData.plugindata.files_filemanager; + if (filesData) { + // Has some data to sync. + let files = filesData.online || [], + promise; + + if (filesData.offline) { + // Has offline files, get them and add them to the list. + promise = this.assignHelper.getStoredSubmissionFiles(assign.id, AddonModAssignSubmissionFileHandler.FOLDER_NAME, + submission.userid, siteId).then((result) => { + files = files.concat(result); + }).catch(() => { + // Folder not found, no files to add. + }); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + return this.assignHelper.uploadFiles(assign.id, files, siteId).then((itemId) => { + pluginData.files_filemanager = itemId; + }); + }); + } + } +} diff --git a/src/addon/mod/assign/submission/onlinetext/component/onlinetext.html b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.html new file mode 100644 index 000000000..e74173388 --- /dev/null +++ b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.html @@ -0,0 +1,21 @@ + + +

{{ plugin.name }}

+

{{ 'addon.mod_assign.numwords' | translate: {'$a': words} }}

+

+ +

+
+ + +
+ {{ plugin.name }} + +

{{ 'addon.mod_assign.wordlimit' | translate }}

+

{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}

+
+ + + + +
diff --git a/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts new file mode 100644 index 000000000..b2c96f266 --- /dev/null +++ b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts @@ -0,0 +1,129 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModAssignProvider } from '../../../providers/assign'; +import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline'; +import { AddonModAssignSubmissionPluginComponent } from '../../../classes/submission-plugin-component'; + +/** + * Component to render an onlinetext submission plugin. + */ +@Component({ + selector: 'addon-mod-assign-submission-online-text', + templateUrl: 'onlinetext.html' +}) +export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignSubmissionPluginComponent implements OnInit { + + control: FormControl; + words: number; + component = AddonModAssignProvider.COMPONENT; + text: string; + loaded: boolean; + + protected wordCountTimeout: any; + protected element: HTMLElement; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider, + element: ElementRef) { + + super(); + this.element = element.nativeElement; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + let promise, + rteEnabled; + + // Check if rich text editor is enabled. + if (this.edit) { + promise = this.domUtils.isRichTextEditorEnabled(); + } else { + // We aren't editing, so no rich text editor. + promise = Promise.resolve(false); + } + + promise.then((enabled) => { + rteEnabled = enabled; + + // Get the text. Check if we have anything offline. + return this.assignOfflineProvider.getSubmission(this.assign.id).catch(() => { + // No offline data found. + }).then((offlineData) => { + if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) { + return offlineData.plugindata.onlinetext_editor.text; + } + + // No offline data found, return online text. + return this.assignProvider.getSubmissionPluginText(this.plugin, this.edit && !rteEnabled); + }); + }).then((text) => { + // We receive them as strings, convert to int. + this.configs.wordlimit = parseInt(this.configs.wordlimit, 10); + this.configs.wordlimitenabled = parseInt(this.configs.wordlimitenabled, 10); + + // Set the text. + this.text = text; + + if (!this.edit) { + // Not editing, see full text when clicked. + this.element.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (text) { + // Open a new state with the interpolated contents. + this.textUtils.expandText(this.plugin.name, text, this.component, this.assign.cmid); + } + }); + } else { + // Create and add the control. + this.control = this.fb.control(text); + } + + // Calculate initial words. + if (this.configs.wordlimitenabled) { + this.words = this.textUtils.countWords(text); + } + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Text changed. + * + * @param {string} text The new text. + */ + onChange(text: string): void { + // Count words if needed. + if (this.configs.wordlimitenabled) { + // Cancel previous wait. + clearTimeout(this.wordCountTimeout); + + // Wait before calculating, if the user keeps inputing we won't calculate. + // This is to prevent slowing down devices, this calculation can be slow if the text is long. + this.wordCountTimeout = setTimeout(() => { + this.words = this.textUtils.countWords(text); + }, 1500); + } + } +} diff --git a/src/addon/mod/assign/submission/onlinetext/lang/en.json b/src/addon/mod/assign/submission/onlinetext/lang/en.json new file mode 100644 index 000000000..9b8a3d9f9 --- /dev/null +++ b/src/addon/mod/assign/submission/onlinetext/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Online text submissions" +} \ No newline at end of file diff --git a/src/addon/mod/assign/submission/onlinetext/onlinetext.module.ts b/src/addon/mod/assign/submission/onlinetext/onlinetext.module.ts new file mode 100644 index 000000000..bed8c8545 --- /dev/null +++ b/src/addon/mod/assign/submission/onlinetext/onlinetext.module.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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModAssignSubmissionOnlineTextHandler } from './providers/handler'; +import { AddonModAssignSubmissionOnlineTextComponent } from './component/onlinetext'; +import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModAssignSubmissionOnlineTextComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModAssignSubmissionOnlineTextHandler + ], + exports: [ + AddonModAssignSubmissionOnlineTextComponent + ], + entryComponents: [ + AddonModAssignSubmissionOnlineTextComponent + ] +}) +export class AddonModAssignSubmissionOnlineTextModule { + constructor(submissionDelegate: AddonModAssignSubmissionDelegate, handler: AddonModAssignSubmissionOnlineTextHandler) { + submissionDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/assign/submission/onlinetext/providers/handler.ts b/src/addon/mod/assign/submission/onlinetext/providers/handler.ts new file mode 100644 index 000000000..4f06dc018 --- /dev/null +++ b/src/addon/mod/assign/submission/onlinetext/providers/handler.ts @@ -0,0 +1,277 @@ + +// (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 { CoreSitesProvider } from '@providers/sites'; +import { CoreWSProvider } from '@providers/ws'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModAssignProvider } from '../../../providers/assign'; +import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline'; +import { AddonModAssignHelperProvider } from '../../../providers/helper'; +import { AddonModAssignSubmissionHandler } from '../../../providers/submission-delegate'; +import { AddonModAssignSubmissionOnlineTextComponent } from '../component/onlinetext'; + +/** + * Handler for online text submission plugin. + */ +@Injectable() +export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssignSubmissionHandler { + name = 'AddonModAssignSubmissionOnlineTextHandler'; + type = 'onlinetext'; + + constructor(private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, + private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, + private assignProvider: AddonModAssignProvider, private assignOfflineProvider: AddonModAssignOfflineProvider, + private assignHelper: AddonModAssignHelperProvider) { } + + /** + * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the + * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit + * unfiltered data. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @return {boolean|Promise} Boolean or promise resolved with boolean: whether it can be edited in offline. + */ + canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise { + // This plugin uses Moodle filters, it cannot be edited in offline. + return false; + } + + /** + * This function will be called when the user wants to create a new submission based on the previous one. + * It should add to pluginData the data to send to server based in the data in plugin (previous attempt). + * + * @param {any} assign The assignment. + * @param {any} plugin The plugin object. + * @param {any} pluginData Object where to store the data to send. + * @param {number} [userId] User ID. If not defined, site's current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + copySubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise { + const text = this.assignProvider.getSubmissionPluginText(plugin, true), + files = this.assignProvider.getSubmissionPluginAttachments(plugin); + let promise; + + if (!files.length) { + // No files to copy, no item ID. + promise = Promise.resolve(0); + } else { + // Re-upload the files. + promise = this.assignHelper.uploadFiles(assign.id, files, siteId); + } + + return promise.then((itemId) => { + pluginData.onlinetext_editor = { + text: text, + format: 1, + itemid: itemId + }; + }); + } + + /** + * Return the Component to use to display the plugin data, either in read or in edit mode. + * 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} plugin The plugin object. + * @param {boolean} [edit] Whether the user is editing. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + return AddonModAssignSubmissionOnlineTextComponent; + } + + /** + * Get files used by this plugin. + * The files returned by this function will be prefetched when the user prefetches the assign. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {any[]|Promise} The files (or promise resolved with the files). + */ + getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise { + return this.assignProvider.getSubmissionPluginAttachments(plugin); + } + + /** + * Get the size of data (in bytes) this plugin will send to copy a previous submission. + * + * @param {any} assign The assignment. + * @param {any} plugin The plugin object. + * @return {number|Promise} The size (or promise resolved with size). + */ + getSizeForCopy(assign: any, plugin: any): number | Promise { + const text = this.assignProvider.getSubmissionPluginText(plugin, true), + files = this.assignProvider.getSubmissionPluginAttachments(plugin), + promises = []; + let totalSize = text.length; + + if (!files.length) { + return totalSize; + } + + files.forEach((file) => { + promises.push(this.wsProvider.getRemoteFileSize(file.fileurl).then((size) => { + if (size == -1) { + // Couldn't determine the size, reject. + return Promise.reject(null); + } + + totalSize += size; + })); + }); + + return Promise.all(promises).then(() => { + return totalSize; + }); + } + + /** + * Get the size of data (in bytes) this plugin will send to add or edit a submission. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + * @return {number|Promise} The size (or promise resolved with size). + */ + getSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): number | Promise { + const text = this.assignProvider.getSubmissionPluginText(plugin, true); + + return text.length; + } + + /** + * Get the text to submit. + * + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + * @return {string} Text to submit. + */ + protected getTextToSubmit(plugin: any, inputData: any): string { + const text = inputData.onlinetext_editor_text, + files = plugin.fileareas && plugin.fileareas[0] ? plugin.fileareas[0].files : []; + + return this.textUtils.restorePluginfileUrls(text, files); + } + + /** + * Check if the submission data has changed for this plugin. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + * @return {boolean|Promise} Boolean (or promise resolved with boolean): whether the data has changed. + */ + hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise { + // Get the original text from plugin or offline. + return this.assignOfflineProvider.getSubmission(assign.id, submission.userid).catch(() => { + // No offline data found. + }).then((data) => { + if (data && data.plugindata && data.plugindata.onlinetext_editor) { + return data.plugindata.onlinetext_editor.text; + } + + // No offline data found, get text from plugin. + return plugin.editorfields && plugin.editorfields[0] ? plugin.editorfields[0].text : ''; + }).then((initialText) => { + // Check if text has changed. + return initialText != this.getTextToSubmit(plugin, inputData); + }); + } + + /** + * 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; + } + + /** + * Whether or not the handler is enabled for edit on a site level. + * + * @return {boolean|Promise} Whether or not the handler is enabled for edit on a site level. + */ + isEnabledForEdit(): boolean | Promise { + // There's a bug in Moodle 3.1.0 that doesn't allow submitting HTML, so we'll disable this plugin in that case. + // Bug was fixed in 3.1.1 minor release and in 3.2. + const currentSite = this.sitesProvider.getCurrentSite(); + + return currentSite.isVersionGreaterEqualThan('3.1.1') || currentSite.checkIfAppUsesLocalMobile(); + } + + /** + * Prepare and add to pluginData the data to send to the server based on the input data. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + * @param {any} pluginData Object where to store the data to send. + * @param {boolean} [offline] Whether the user is editing in offline. + * @param {number} [userId] User ID. If not defined, site's current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + prepareSubmissionData(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean, + userId?: number, siteId?: string): void | Promise { + + return this.domUtils.isRichTextEditorEnabled().then((enabled) => { + let text = this.getTextToSubmit(plugin, inputData); + if (!enabled) { + // Rich text editor not enabled, add some HTML to the text if needed. + text = this.textUtils.formatHtmlLines(text); + } + + pluginData.onlinetext_editor = { + text: text, + format: 1, + itemid: 0 // Can't add new files yet, so we use a fake itemid. + }; + }); + } + + /** + * Prepare and add to pluginData the data to send to the server based on the offline data stored. + * This will be used when performing a synchronization. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} offlineData Offline data stored. + * @param {any} pluginData Object where to store the data to send. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + prepareSyncData(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string) + : void | Promise { + + const textData = offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor; + if (textData) { + // Has some data to sync. + pluginData.onlinetext_editor = textData; + } + } +} diff --git a/src/addon/mod/assign/submission/submission.module.ts b/src/addon/mod/assign/submission/submission.module.ts new file mode 100644 index 000000000..a95495457 --- /dev/null +++ b/src/addon/mod/assign/submission/submission.module.ts @@ -0,0 +1,31 @@ +// (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 { AddonModAssignSubmissionCommentsModule } from './comments/comments.module'; +import { AddonModAssignSubmissionFileModule } from './file/file.module'; +import { AddonModAssignSubmissionOnlineTextModule } from './onlinetext/onlinetext.module'; + +@NgModule({ + declarations: [], + imports: [ + AddonModAssignSubmissionCommentsModule, + AddonModAssignSubmissionFileModule, + AddonModAssignSubmissionOnlineTextModule + ], + providers: [ + ], + exports: [] +}) +export class AddonModAssignSubmissionModule { } diff --git a/src/app/app.scss b/src/app/app.scss index 642918b05..624b27ebd 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -81,6 +81,12 @@ background-color: $gray-lighter; } +// Make no-lines work in any element, not just ion-item and ion-list. +.item *[no-lines] .item-inner, +*[no-lines] .item .item-inner { + border: 0; +} + .core-oauth-icon, .item.core-oauth-icon, .list .item.core-oauth-icon { min-height: 32px; img, .label { diff --git a/src/components/attachments/attachments.html b/src/components/attachments/attachments.html new file mode 100644 index 000000000..7343fc4f5 --- /dev/null +++ b/src/components/attachments/attachments.html @@ -0,0 +1,25 @@ + + {{ 'core.maxsizeandattachments' | translate:{$a: {size: maxSizeReadable, attachments: maxSubmissionsReadable} } }} + + +

{{ 'core.fileuploader.filesofthesetypes' | translate }}

+
    +
  • + {{typeInfo.name}} {{typeInfo.extlist}} +
  • +
+
+
+ + + + + +
+ + + + + {{ 'core.fileuploader.addfiletext' | translate }} + + \ No newline at end of file diff --git a/src/components/attachments/attachments.ts b/src/components/attachments/attachments.ts new file mode 100644 index 000000000..71f120dc9 --- /dev/null +++ b/src/components/attachments/attachments.ts @@ -0,0 +1,135 @@ +// (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 } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper'; + +/** + * Component to render attachments, allow adding more and delete the current ones. + * + * All the changes done will be applied to the "files" input array, no file will be uploaded. The component using this + * component should be the one uploading and moving the files. + * + * All the files added will be copied to the app temporary folder, so they should be deleted after uploading them + * or if the user cancels the action. + * + * + * + */ +@Component({ + selector: 'core-attachments', + templateUrl: 'attachments.html' +}) +export class CoreAttachmentsComponent implements OnInit { + @Input() files: any[]; // List of attachments. New attachments will be added to this array. + @Input() maxSize: number; // Max size for attachments. If not defined, 0 or -1, unknown size. + @Input() maxSubmissions: number; // Max number of attachments. If -1 or not defined, unknown limit. + @Input() component: string; // Component the downloaded files will be linked to. + @Input() componentId: string | number; // Component ID. + @Input() allowOffline: boolean | string; // Whether to allow selecting files in offline. + @Input() acceptedTypes: string; // List of supported filetypes. If undefined, all types supported. + + maxSizeReadable: string; + maxSubmissionsReadable: string; + unlimitedFiles: boolean; + + protected fileTypes: { info: any[], mimetypes: string[] }; + + constructor(protected appProvider: CoreAppProvider, protected domUtils: CoreDomUtilsProvider, + protected textUtils: CoreTextUtilsProvider, protected fileUploaderProvider: CoreFileUploaderProvider, + protected translate: TranslateService, protected fileUploaderHelper: CoreFileUploaderHelperProvider) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.maxSize = Number(this.maxSize); // Make sure it's defined and it's a number. + this.maxSize = !isNaN(this.maxSize) && this.maxSize > 0 ? this.maxSize : -1; + + if (this.maxSize == -1) { + this.maxSizeReadable = this.translate.instant('core.unknown'); + } else { + this.maxSizeReadable = this.textUtils.bytesToSize(this.maxSize, 2); + } + + if (typeof this.maxSubmissions == 'undefined' || this.maxSubmissions < 0) { + this.maxSubmissionsReadable = this.translate.instant('core.unknown'); + this.unlimitedFiles = true; + } else { + this.maxSubmissionsReadable = String(this.maxSubmissions); + } + + if (this.acceptedTypes && this.acceptedTypes.trim()) { + this.fileTypes = this.fileUploaderProvider.prepareFiletypeList(this.acceptedTypes); + } + } + + /** + * Add a new attachment. + */ + add(): void { + const allowOffline = this.allowOffline && this.allowOffline !== 'false'; + + if (!allowOffline && !this.appProvider.isOnline()) { + this.domUtils.showErrorModal('core.fileuploader.errormustbeonlinetoupload', true); + } else { + const mimetypes = this.fileTypes && this.fileTypes.mimetypes; + + this.fileUploaderHelper.selectFile(this.maxSize, allowOffline, undefined, mimetypes).then((result) => { + this.files.push(result); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error selecting file.'); + }); + } + } + + /** + * Delete a file from the list. + * + * @param {number} index The index of the file. + * @param {boolean} [askConfirm] Whether to ask confirm. + */ + delete(index: number, askConfirm?: boolean): void { + let promise; + + if (askConfirm) { + promise = this.domUtils.showConfirm(this.translate.instant('core.confirmdeletefile')); + } else { + promise = Promise.resolve(); + } + + promise.then(() => { + // Remove the file from the list. + this.files.splice(index, 1); + }).catch(() => { + // User cancelled. + }); + } + + /** + * A file was renamed. + * + * @param {number} index Index of the file. + * @param {any} file The new file entry. + */ + renamed(index: number, file: any): void { + this.files[index] = file; + } +} diff --git a/src/components/components.module.ts b/src/components/components.module.ts index 11f1b3efa..4c50dea20 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -43,6 +43,7 @@ import { CoreSendMessageFormComponent } from './send-message-form/send-message-f import { CoreTimerComponent } from './timer/timer'; import { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha/recaptcha'; import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar'; +import { CoreAttachmentsComponent } from './attachments/attachments'; @NgModule({ declarations: [ @@ -72,7 +73,8 @@ import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar'; CoreTimerComponent, CoreRecaptchaComponent, CoreRecaptchaModalComponent, - CoreNavigationBarComponent + CoreNavigationBarComponent, + CoreAttachmentsComponent ], entryComponents: [ CoreContextMenuPopoverComponent, @@ -109,7 +111,8 @@ import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar'; CoreSendMessageFormComponent, CoreTimerComponent, CoreRecaptchaComponent, - CoreNavigationBarComponent + CoreNavigationBarComponent, + CoreAttachmentsComponent ] }) export class CoreComponentsModule {} diff --git a/src/components/file/file.ts b/src/components/file/file.ts index ab0585190..a073e2d59 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -39,7 +39,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { @Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded. // Use it for files that you cannot determine if they're outdated or not. @Input() canDownload?: boolean | string = true; // Whether file can be downloaded. - @Output() onDelete?: EventEmitter; // Will notify when the delete button is clicked. + @Output() onDelete?: EventEmitter; // Will notify when the delete button is clicked. isDownloaded: boolean; isDownloading: boolean; @@ -178,7 +178,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { * * @param {Event} e Click event. */ - deleteFile(e: Event): void { + delete(e: Event): void { e.preventDefault(); e.stopPropagation(); diff --git a/src/components/local-file/local-file.ts b/src/components/local-file/local-file.ts index 1acdcbd5d..ac7284306 100644 --- a/src/components/local-file/local-file.ts +++ b/src/components/local-file/local-file.ts @@ -177,8 +177,8 @@ export class CoreLocalFileComponent implements OnInit { }).finally(() => { modal.dismiss(); }); - }).catch(() => { - this.domUtils.showErrorModal('core.errordeletefile', true); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.errordeletefile', true); }); } } diff --git a/src/core/fileuploader/providers/fileuploader.ts b/src/core/fileuploader/providers/fileuploader.ts index 4934ead60..4effe967c 100644 --- a/src/core/fileuploader/providers/fileuploader.ts +++ b/src/core/fileuploader/providers/fileuploader.ts @@ -487,7 +487,7 @@ export class CoreFileUploaderProvider { * @return {Promise} Promise resolved with the itemId. */ uploadOrReuploadFiles(files: any[], component?: string, componentId?: string | number, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (!files || !files.length) { // Return fake draft ID.