From 85f79bb9440a0491b201f21d7243ad0f41fb8434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 12 Feb 2021 16:43:59 +0100 Subject: [PATCH] MOBILE-3636 assign: Submission plugins --- src/addons/mod/assign/assign.module.ts | 2 + .../submission-plugin/submission-plugin.ts | 3 +- .../components/submission/submission.ts | 2 +- .../mod/assign/services/assign-helper.ts | 10 +- src/addons/mod/assign/services/assign.ts | 3 +- .../mod/assign/services/feedback-delegate.ts | 3 +- .../assign/services/submission-delegate.ts | 45 +- .../submission/comments/comments.module.ts | 48 +++ .../addon-mod-assign-submission-comments.html | 8 + .../submission/comments/component/comments.ts | 61 +++ .../mod/assign/submission/comments/lang.json | 3 + .../submission/comments/services/handler.ts | 107 +++++ .../addon-mod-assign-submission-file.html | 19 + .../assign/submission/file/component/file.ts | 85 ++++ .../mod/assign/submission/file/file.module.ts | 46 +++ .../mod/assign/submission/file/lang.json | 3 + .../submission/file/services/handler.ts | 388 ++++++++++++++++++ ...ddon-mod-assign-submission-onlinetext.html | 35 ++ .../onlinetext/component/onlinetext.ts | 130 ++++++ .../assign/submission/onlinetext/lang.json | 4 + .../onlinetext/onlinetext.module.ts | 48 +++ .../submission/onlinetext/services/handler.ts | 323 +++++++++++++++ .../assign/submission/submission.module.ts | 27 ++ 23 files changed, 1371 insertions(+), 32 deletions(-) create mode 100644 src/addons/mod/assign/submission/comments/comments.module.ts create mode 100644 src/addons/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html create mode 100644 src/addons/mod/assign/submission/comments/component/comments.ts create mode 100644 src/addons/mod/assign/submission/comments/lang.json create mode 100644 src/addons/mod/assign/submission/comments/services/handler.ts create mode 100644 src/addons/mod/assign/submission/file/component/addon-mod-assign-submission-file.html create mode 100644 src/addons/mod/assign/submission/file/component/file.ts create mode 100644 src/addons/mod/assign/submission/file/file.module.ts create mode 100644 src/addons/mod/assign/submission/file/lang.json create mode 100644 src/addons/mod/assign/submission/file/services/handler.ts create mode 100644 src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html create mode 100644 src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts create mode 100644 src/addons/mod/assign/submission/onlinetext/lang.json create mode 100644 src/addons/mod/assign/submission/onlinetext/onlinetext.module.ts create mode 100644 src/addons/mod/assign/submission/onlinetext/services/handler.ts create mode 100644 src/addons/mod/assign/submission/submission.module.ts diff --git a/src/addons/mod/assign/assign.module.ts b/src/addons/mod/assign/assign.module.ts index 1a7565342..fbf319570 100644 --- a/src/addons/mod/assign/assign.module.ts +++ b/src/addons/mod/assign/assign.module.ts @@ -29,6 +29,7 @@ import { AddonModAssignModuleHandler, AddonModAssignModuleHandlerService } from import { AddonModAssignPrefetchHandler } from './services/handlers/prefetch'; import { AddonModAssignPushClickHandler } from './services/handlers/push-click'; import { AddonModAssignSyncCronHandler } from './services/handlers/sync-cron'; +import { AddonModAssignSubmissionModule } from './submission/submission.module'; const routes: Routes = [ { @@ -41,6 +42,7 @@ const routes: Routes = [ imports: [ CoreMainMenuTabRoutingModule.forChild(routes), AddonModAssignComponentsModule, + AddonModAssignSubmissionModule, ], providers: [ { diff --git a/src/addons/mod/assign/components/submission-plugin/submission-plugin.ts b/src/addons/mod/assign/components/submission-plugin/submission-plugin.ts index 5507f6273..fcbb25bec 100644 --- a/src/addons/mod/assign/components/submission-plugin/submission-plugin.ts +++ b/src/addons/mod/assign/components/submission-plugin/submission-plugin.ts @@ -24,6 +24,7 @@ import { } from '../../services/assign'; import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper'; import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate'; +import { FileEntry } from '@ionic-native/file/ngx'; /** * Component that displays an assignment submission plugin. @@ -48,7 +49,7 @@ export class AddonModAssignSubmissionPluginComponent implements OnInit { // Data to render the plugin if it isn't supported. component = AddonModAssignProvider.COMPONENT; text = ''; - files: CoreWSExternalFile[] = []; + files: (FileEntry | CoreWSExternalFile)[] = []; notSupported = false; pluginLoaded = false; diff --git a/src/addons/mod/assign/components/submission/submission.ts b/src/addons/mod/assign/components/submission/submission.ts index e4e2114e5..0015d71e8 100644 --- a/src/addons/mod/assign/components/submission/submission.ts +++ b/src/addons/mod/assign/components/submission/submission.ts @@ -392,7 +392,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { try { return AddonModAssignHelper.instance.hasFeedbackDataChanged( this.assign!, - this.userSubmission, + this.userSubmission!, // @todo this.feedback, this.submitId, ); diff --git a/src/addons/mod/assign/services/assign-helper.ts b/src/addons/mod/assign/services/assign-helper.ts index fa84c6885..6db331096 100644 --- a/src/addons/mod/assign/services/assign-helper.ts +++ b/src/addons/mod/assign/services/assign-helper.ts @@ -492,7 +492,7 @@ export class AddonModAssignHelperProvider { */ async hasFeedbackDataChanged( assign: AddonModAssignAssign, - submission: AddonModAssignSubmission, + submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted, feedback: AddonModAssignSubmissionFeedback, userId: number, ): Promise { @@ -683,15 +683,13 @@ export class AddonModAssignHelperProvider { offline = false, userId?: number, siteId?: string, - ): Promise { + ): Promise { if (offline) { - await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId); - - return; + return await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId); } - await this.uploadFiles(assignId, files, siteId); + return await this.uploadFiles(assignId, files, siteId); } } diff --git a/src/addons/mod/assign/services/assign.ts b/src/addons/mod/assign/services/assign.ts index 2e793647f..1c8973075 100644 --- a/src/addons/mod/assign/services/assign.ts +++ b/src/addons/mod/assign/services/assign.ts @@ -29,6 +29,7 @@ import { CoreApp } from '@services/app'; import { CoreUtils } from '@services/utils/utils'; import { AddonModAssignOffline } from './assign-offline'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; +import { CoreComments } from '@features/comments/services/comments'; const ROOT_CACHE_KEY = 'mmaModAssign:'; @@ -754,7 +755,7 @@ export class AddonModAssignProvider { promises.push(this.invalidateAssignmentUserMappingsData(assign.id, siteId)); promises.push(this.invalidateAssignmentGradesData(assign.id, siteId)); promises.push(this.invalidateListParticipantsData(assign.id, siteId)); - // @todo promises.push(CoreComments.instance.invalidateCommentsByInstance('module', assign.id, siteId)); + promises.push(CoreComments.instance.invalidateCommentsByInstance('module', assign.id, siteId)); promises.push(this.invalidateAssignmentData(courseId, siteId)); promises.push(CoreGrades.instance.invalidateAllCourseGradesData(courseId)); diff --git a/src/addons/mod/assign/services/feedback-delegate.ts b/src/addons/mod/assign/services/feedback-delegate.ts index 978739474..8f78851ff 100644 --- a/src/addons/mod/assign/services/feedback-delegate.ts +++ b/src/addons/mod/assign/services/feedback-delegate.ts @@ -18,6 +18,7 @@ import { AddonModAssignDefaultFeedbackHandler } from './handlers/default-feedbac import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign'; import { makeSingleton } from '@singletons'; import { CoreWSExternalFile } from '@services/ws'; +import { AddonModAssignSubmissionFormatted } from './assign-helper'; /** * Interface that all feedback handlers must implement. @@ -264,7 +265,7 @@ export class AddonModAssignFeedbackDelegateService extends CoreDelegate, ): void; /** @@ -105,9 +106,9 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - offlineData: any, + offlineData: AddonModAssignSubmissionsDBRecordFormatted, siteId?: string, - ): void | Promise; + ): void | Promise; /** * Return the Component to use to display the plugin data, either in read or in edit mode. @@ -172,7 +173,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - inputData: any, + inputData: Record, ): number | Promise; /** @@ -188,7 +189,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - inputData: any, + inputData: Record, ): boolean | Promise; /** @@ -232,12 +233,12 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - inputData: any, + inputData: Record, pluginData: AddonModAssignSavePluginData, offline?: boolean, userId?: number, siteId?: string, - ): void | Promise; + ): void | Promise; /** * Prepare and add to pluginData the data to send to the server based on the offline data stored. @@ -255,10 +256,10 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - offlineData: any, - pluginData: any, + offlineData: AddonModAssignSubmissionsDBRecordFormatted, + pluginData: AddonModAssignSavePluginData, siteId?: string, - ): void | Promise; + ): void | Promise; } /** @@ -303,7 +304,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate, ): void { return this.executeFunctionOnEnabled(plugin.type, 'clearTmpData', [assign, submission, plugin, inputData]); } @@ -346,9 +347,9 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate { + ): Promise { return await this.executeFunctionOnEnabled( plugin.type, 'deleteOfflineData', @@ -423,7 +424,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate, ): Promise { return await this.executeFunctionOnEnabled( plugin.type, @@ -445,7 +446,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate, ): Promise { return await this.executeFunctionOnEnabled( plugin.type, @@ -520,12 +521,12 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate, + pluginData: AddonModAssignSavePluginData, offline?: boolean, userId?: number, siteId?: string, - ): Promise { + ): Promise { return await this.executeFunctionOnEnabled( plugin.type, @@ -549,10 +550,10 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate { + ): Promise { return this.executeFunctionOnEnabled( plugin.type, @@ -562,4 +563,4 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate () => { + AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionCommentsHandler.instance); + }, + }, + ], + exports: [ + AddonModAssignSubmissionCommentsComponent, + ], + entryComponents: [ + AddonModAssignSubmissionCommentsComponent, + ], +}) +export class AddonModAssignSubmissionCommentsModule {} diff --git a/src/addons/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html b/src/addons/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html new file mode 100644 index 000000000..d23d23601 --- /dev/null +++ b/src/addons/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html @@ -0,0 +1,8 @@ + + +

{{plugin.name}}

+ + +
+
diff --git a/src/addons/mod/assign/submission/comments/component/comments.ts b/src/addons/mod/assign/submission/comments/component/comments.ts new file mode 100644 index 000000000..38bf30cc0 --- /dev/null +++ b/src/addons/mod/assign/submission/comments/component/comments.ts @@ -0,0 +1,61 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin'; +import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments'; +import { CoreComments } from '@features/comments/services/comments'; + +/** + * Component to render a comments submission plugin. + */ +@Component({ + selector: 'addon-mod-assign-submission-comments', + templateUrl: 'addon-mod-assign-submission-comments.html', +}) +export class AddonModAssignSubmissionCommentsComponent extends AddonModAssignSubmissionPluginComponent { + + @ViewChild(CoreCommentsCommentsComponent) commentsComponent!: CoreCommentsCommentsComponent; + + commentsEnabled: boolean; + + constructor() { + super(); + + this.commentsEnabled = !CoreComments.instance.areCommentsDisabledInSite(); + } + + /** + * Invalidate the data. + * + * @return Promise resolved when done. + */ + invalidate(): Promise { + return CoreComments.instance.invalidateCommentsData( + 'module', + this.assign.cmid, + 'assignsubmission_comments', + this.submission.id, + 'submission_comments', + ); + } + + /** + * Show the comments. + */ + showComments(e?: Event): void { + this.commentsComponent?.openComments(e); + } + +} diff --git a/src/addons/mod/assign/submission/comments/lang.json b/src/addons/mod/assign/submission/comments/lang.json new file mode 100644 index 000000000..c69c732aa --- /dev/null +++ b/src/addons/mod/assign/submission/comments/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Submission comments" +} \ No newline at end of file diff --git a/src/addons/mod/assign/submission/comments/services/handler.ts b/src/addons/mod/assign/submission/comments/services/handler.ts new file mode 100644 index 000000000..b32bd290d --- /dev/null +++ b/src/addons/mod/assign/submission/comments/services/handler.ts @@ -0,0 +1,107 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from '@addons/mod/assign/services/assign'; +import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate'; +import { Injectable, Type } from '@angular/core'; +import { CoreComments } from '@features/comments/services/comments'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; +import { AddonModAssignSubmissionCommentsComponent } from '../component/comments'; + +/** + * Handler for comments submission plugin. + */ +@Injectable( { providedIn: 'root' }) +export class AddonModAssignSubmissionCommentsHandlerService implements AddonModAssignSubmissionHandler { + + name = 'AddonModAssignSubmissionCommentsHandler'; + type = 'comments'; + + + /** + * 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. + * + * @return Boolean or promise resolved with boolean: whether it can be edited in offline. + */ + canEditOffline(): boolean { + // 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 plugin The plugin object. + * @param edit Whether the user is editing. + * @return The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(plugin: AddonModAssignPlugin, edit = false): Type | undefined { + return edit ? undefined : AddonModAssignSubmissionCommentsComponent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return True or promise resolved with true if enabled. + */ + async isEnabled(): Promise { + return true; + } + + /** + * Whether or not the handler is enabled for edit on a site level. + * + * @return Whether or not the handler is enabled for edit on a site level. + */ + isEnabledForEdit(): boolean{ + 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 assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async prefetch( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + siteId?: string, + ): Promise { + + // Fail silently (Moodle < 3.1.1, 3.2) + await CoreUtils.instance.ignoreErrors( + CoreComments.instance.getComments( + 'module', + assign.cmid, + 'assignsubmission_comments', + submission.id, + 'submission_comments', + 0, + siteId, + ), + ); + } + +} +export const AddonModAssignSubmissionCommentsHandler = makeSingleton(AddonModAssignSubmissionCommentsHandlerService); diff --git a/src/addons/mod/assign/submission/file/component/addon-mod-assign-submission-file.html b/src/addons/mod/assign/submission/file/component/addon-mod-assign-submission-file.html new file mode 100644 index 000000000..92517dd92 --- /dev/null +++ b/src/addons/mod/assign/submission/file/component/addon-mod-assign-submission-file.html @@ -0,0 +1,19 @@ + + + +

{{ plugin.name }}

+
+ +
+
+
+ + +
+ +

{{ plugin.name }}

+
+ + +
diff --git a/src/addons/mod/assign/submission/file/component/file.ts b/src/addons/mod/assign/submission/file/component/file.ts new file mode 100644 index 000000000..2dff6e98a --- /dev/null +++ b/src/addons/mod/assign/submission/file/component/file.ts @@ -0,0 +1,85 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin'; +import { AddonModAssign, AddonModAssignProvider } from '@addons/mod/assign/services/assign'; +import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper'; +import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline'; +import { Component, OnInit } from '@angular/core'; +import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; +import { CoreFileSession } from '@services/file-session'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonModAssignSubmissionFileHandlerService } from '../services/handler'; +import { FileEntry } from '@ionic-native/file/ngx'; + +/** + * Component to render a file submission plugin. + */ +@Component({ + selector: 'addon-mod-assign-submission-file', + templateUrl: 'addon-mod-assign-submission-file.html', +}) +export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmissionPluginComponent implements OnInit { + + component = AddonModAssignProvider.COMPONENT; + + maxSize?: number; + acceptedTypes?: string; + maxSubmissions?: number; + + /** + * Component being initialized. + */ + async nOnInit(): Promise { + // Get the offline data. + const filesData = await CoreUtils.instance.ignoreErrors( + AddonModAssignOffline.instance.getSubmission(this.assign.id), + undefined, + ); + + this.acceptedTypes = this.data?.configs.filetypeslist; + this.maxSize = this.data?.configs.maxsubmissionsizebytes + ? parseInt(this.data?.configs.maxsubmissionsizebytes, 10) + : undefined; + this.maxSubmissions = this.data?.configs.maxfilesubmissions + ? parseInt(this.data?.configs.maxfilesubmissions, 10) + : undefined; + + try { + if (filesData && filesData.plugindata && filesData.plugindata.files_filemanager) { + const offlineDataFiles = filesData.plugindata.files_filemanager; + // It has offline data. + let offlineFiles: FileEntry[] = []; + if (offlineDataFiles.offline) { + offlineFiles = await CoreUtils.instance.ignoreErrors( + AddonModAssignHelper.instance.getStoredSubmissionFiles( + this.assign.id, + AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, + ), + [], + ); + } + + this.files = offlineDataFiles.online || []; + this.files = this.files.concat(offlineFiles); + } else { + // No offline data, get the online files. + this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin); + } + } finally { + CoreFileSession.instance.setFiles(this.component, this.assign.id, this.files); + } + } + +} diff --git a/src/addons/mod/assign/submission/file/file.module.ts b/src/addons/mod/assign/submission/file/file.module.ts new file mode 100644 index 000000000..cf97e85f7 --- /dev/null +++ b/src/addons/mod/assign/submission/file/file.module.ts @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { AddonModAssignSubmissionFileHandler } from './services/handler'; +import { AddonModAssignSubmissionFileComponent } from './component/file'; +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate'; + +@NgModule({ + declarations: [ + AddonModAssignSubmissionFileComponent, + ], + imports: [ + CoreSharedModule, + ], + providers: [ + AddonModAssignSubmissionFileHandler, + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionFileHandler.instance); + }, + }, + ], + exports: [ + AddonModAssignSubmissionFileComponent, + ], + entryComponents: [ + AddonModAssignSubmissionFileComponent, + ], +}) +export class AddonModAssignSubmissionFileModule {} diff --git a/src/addons/mod/assign/submission/file/lang.json b/src/addons/mod/assign/submission/file/lang.json new file mode 100644 index 000000000..7262ba217 --- /dev/null +++ b/src/addons/mod/assign/submission/file/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "File submissions" +} \ No newline at end of file diff --git a/src/addons/mod/assign/submission/file/services/handler.ts b/src/addons/mod/assign/submission/file/services/handler.ts new file mode 100644 index 000000000..344b8cc3a --- /dev/null +++ b/src/addons/mod/assign/submission/file/services/handler.ts @@ -0,0 +1,388 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { + AddonModAssignAssign, + AddonModAssignSubmission, + AddonModAssignPlugin, + AddonModAssignProvider, + AddonModAssign, +} from '@addons/mod/assign/services/assign'; +import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper'; +import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } from '@addons/mod/assign/services/assign-offline'; +import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate'; +import { Injectable, Type } from '@angular/core'; +import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; +import { CoreFileHelper } from '@services/file-helper'; +import { CoreFileSession } from '@services/file-session'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { AddonModAssignSubmissionFileComponent } from '../component/file'; +import { FileEntry } from '@ionic-native/file/ngx'; + +/** + * Handler for file submission plugin. + */ +@Injectable( { providedIn: 'root' }) +export class AddonModAssignSubmissionFileHandlerService implements AddonModAssignSubmissionHandler { + + static readonly FOLDER_NAME = 'submission_file'; + + name = 'AddonModAssignSubmissionFileHandler'; + type = 'file'; + + /** + * 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. + * + * @return Boolean or promise resolved with boolean: whether it can be edited in offline. + */ + canEditOffline(): boolean { + // This plugin doesn't use Moodle filters, it can be edited in offline. + return true; + } + + /** + * Check if a plugin has no data. + * + * @param assign The assignment. + * @param plugin The plugin object. + * @return Whether the plugin is empty. + */ + isEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean { + const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); + + return files.length === 0; + } + + /** + * Should clear temporary data for a cancelled submission. + * + * @param assign The assignment. + */ + clearTmpData(assign: AddonModAssignAssign): void { + const files = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id); + + // Clear the files in session for this assign. + CoreFileSession.instance.clearFiles(AddonModAssignProvider.COMPONENT, assign.id); + + // Now delete the local files from the tmp folder. + CoreFileUploader.instance.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 assign The assignment. + * @param plugin The plugin object. + * @param pluginData Object where to store the data to send. + * @return If the function is async, it should return a Promise resolved when done. + */ + async copySubmissionData( + assign: AddonModAssignAssign, + plugin: AddonModAssignPlugin, + pluginData: AddonModAssignSubmissionFilePluginData, + ): Promise { + // We need to re-upload all the existing files. + const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); + + // Get the itemId. + pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadFiles(assign.id, files); + } + + /** + * 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. + * + * @return The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(): Type { + return AddonModAssignSubmissionFileComponent; + } + + /** + * Delete any stored data for the plugin and submission. + * + * @param assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @param offlineData Offline data stored. + * @param siteId Site ID. If not defined, current site. + * @return If the function is async, it should return a Promise resolved when done. + */ + async deleteOfflineData( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + offlineData: AddonModAssignSubmissionsDBRecordFormatted, + siteId?: string, + ): Promise { + + await CoreUtils.instance.ignoreErrors( + AddonModAssignHelper.instance.deleteStoredSubmissionFiles( + assign.id, + AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, + submission.userid, + siteId, + ), + ); + } + + /** + * Get files used by this plugin. + * The files returned by this function will be prefetched when the user prefetches the assign. + * + * @param assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @param siteId Site ID. If not defined, current site. + * @return The files (or promise resolved with the files). + */ + getPluginFiles( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + ): CoreWSExternalFile[] { + return AddonModAssign.instance.getSubmissionPluginAttachments(plugin); + } + + /** + * Get the size of data (in bytes) this plugin will send to copy a previous submission. + * + * @param assign The assignment. + * @param plugin The plugin object. + * @return The size (or promise resolved with size). + */ + async getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise { + const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); + + return CoreFileHelper.instance.getTotalFilesSize(files); + } + + /** + * Get the size of data (in bytes) this plugin will send to add or edit a submission. + * + * @param assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @return The size (or promise resolved with size). + */ + async getSizeForEdit( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + ): Promise { + // Check if there's any change. + if (this.hasDataChanged(assign, submission, plugin)) { + const files = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id); + + return CoreFileHelper.instance.getTotalFilesSize(files); + } else { + // Nothing has changed, we won't upload any file. + return 0; + } + } + + /** + * Check if the submission data has changed for this plugin. + * + * @param assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @return Boolean (or promise resolved with boolean): whether the data has changed. + */ + async hasDataChanged( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + ): Promise { + const offlineData = await CoreUtils.instance.ignoreErrors( + // Check if there's any offline data. + AddonModAssignOffline.instance.getSubmission(assign.id, submission.userid), + undefined, + ); + + let numFiles: number; + if (offlineData && offlineData.plugindata && offlineData.plugindata.files_filemanager) { + const offlineDataFiles = offlineData.plugindata.files_filemanager; + // Has offline data, return the number of files. + numFiles = offlineDataFiles.offline + offlineDataFiles.online.length; + } else { + // No offline data, return the number of online files. + const pluginFiles = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); + + numFiles = pluginFiles && pluginFiles.length; + } + + const currentFiles = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id); + + if (currentFiles.length != numFiles) { + // Number of files has changed. + return true; + } + + const files = await this.getSubmissionFilesToSync(assign, submission, offlineData); + + // Check if there is any local file added and list has changed. + return CoreFileUploader.instance.areFileListDifferent(currentFiles, files); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return True or promise resolved with true if enabled. + */ + async isEnabled(): Promise { + return true; + } + + /** + * Whether or not the handler is enabled for edit on a site level. + * + * @return Whether or not the handler is enabled for edit on a site level. + */ + isEnabledForEdit(): boolean { + return true; + } + + /** + * Prepare and add to pluginData the data to send to the server based on the input data. + * + * @param assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @param inputData Data entered by the user for the submission. + * @param pluginData Object where to store the data to send. + * @param offline Whether the user is editing in offline. + * @param userId User ID. If not defined, site's current user. + * @param siteId Site ID. If not defined, current site. + * @return If the function is async, it should return a Promise resolved when done. + */ + async prepareSubmissionData( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + inputData: AddonModAssignSubmissionFileData, + pluginData: AddonModAssignSubmissionFilePluginData, + offline?: boolean, + userId?: number, + siteId?: string, + ): Promise { + + const changed = await this.hasDataChanged(assign, submission, plugin); + if (!changed) { + return; + } + + // Data has changed, we need to upload new files and re-upload all the existing files. + const currentFiles = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id); + const error = CoreUtils.instance.hasRepeatedFilenames(currentFiles); + + if (error) { + throw error; + } + + pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadOrStoreFiles( + assign.id, + AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, + currentFiles, + offline, + userId, + siteId, + ); + } + + /** + * 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 assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @param offlineData Offline data stored. + * @param pluginData Object where to store the data to send. + * @param siteId Site ID. If not defined, current site. + * @return If the function is async, it should return a Promise resolved when done. + */ + async prepareSyncData( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + offlineData: AddonModAssignSubmissionsDBRecordFormatted, + pluginData: AddonModAssignSubmissionFilePluginData, + siteId?: string, + ): Promise { + + const files = await this.getSubmissionFilesToSync(assign, submission, offlineData, siteId); + + if (files.length == 0) { + return; + } + + pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadFiles(assign.id, files, siteId); + } + + /** + * Get the file list to be synced. + * + * @param assign The assignment. + * @param submission The submission. + * @param offlineData Offline data stored. + * @param siteId Site ID. If not defined, current site. + * @return File entries when is all resolved. + */ + protected async getSubmissionFilesToSync( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + offlineData?: AddonModAssignSubmissionsDBRecordFormatted, + siteId?: string, + ): Promise<(FileEntry | CoreWSExternalFile)[]> { + const filesData = offlineData?.plugindata.files_filemanager; + if (!filesData) { + return []; + } + + // Has some data to sync. + let files: (FileEntry | CoreWSExternalFile)[] = filesData.online || []; + + if (filesData.offline) { + // Has offline files, get them and add them to the list. + const storedFiles = await CoreUtils.instance.ignoreErrors( + AddonModAssignHelper.instance.getStoredSubmissionFiles( + assign.id, + AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, + submission.userid, + siteId, + ), + [], + ); + files = files.concat(storedFiles); + } + + return files; + } + +} +export const AddonModAssignSubmissionFileHandler = makeSingleton(AddonModAssignSubmissionFileHandlerService); + +// Define if ever used. +export type AddonModAssignSubmissionFileData = Record; + +export type AddonModAssignSubmissionFilePluginData = { + // The id of a draft area containing files for this submission. Or the offline file results. + files_filemanager: number | CoreFileUploaderStoreFilesResult; // eslint-disable-line @typescript-eslint/naming-convention +}; diff --git a/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html b/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html new file mode 100644 index 000000000..cc3c5c014 --- /dev/null +++ b/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html @@ -0,0 +1,35 @@ + + + +

{{ plugin.name }}

+

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

+

+ + +

+
+
+ + +
+ +

{{ plugin.name }}

+
+ + +

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

+

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

+
+
+ + + + + + +
diff --git a/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts b/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts new file mode 100644 index 000000000..b8a5b38ac --- /dev/null +++ b/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts @@ -0,0 +1,130 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin'; +import { AddonModAssignProvider, AddonModAssign } from '@addons/mod/assign/services/assign'; +import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline'; +import { Component, OnInit, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonModAssignSubmissionOnlineTextPluginData } from '../services/handler'; + + +/** + * Component to render an onlinetext submission plugin. + */ +@Component({ + selector: 'addon-mod-assign-submission-online-text', + templateUrl: 'addon-mod-assign-submission-onlinetext.html', +}) +export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignSubmissionPluginComponent implements OnInit { + + control?: FormControl; + words = 0; + component = AddonModAssignProvider.COMPONENT; + text = ''; + loaded = false; + wordLimitEnabled = false; + currentUserId: number; + wordLimit = 0; + + protected wordCountTimeout?: number; + protected element: HTMLElement; + + constructor( + protected fb: FormBuilder, + element: ElementRef, + ) { + super(); + this.element = element.nativeElement; + this.currentUserId = CoreSites.instance.getCurrentSiteUserId(); + } + + /** + * Component being initialized. + */ + async nOnInit(): Promise { + // Get the text. Check if we have anything offline. + const offlineData = await CoreUtils.instance.ignoreErrors( + AddonModAssignOffline.instance.getSubmission(this.assign.id), + undefined, + ); + + this.wordLimitEnabled = !!parseInt(this.data?.configs.wordlimitenabled || '0', 10); + this.wordLimit = parseInt(this.data?.configs.wordlimit || '0'); + + try { + if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) { + this.text = (offlineData.plugindata).onlinetext_editor.text; + } else { + // No offline data found, return online text. + this.text = AddonModAssign.instance.getSubmissionPluginText(this.plugin); + } + + + // Set the text. + if (!this.edit) { + // Not editing, see full text when clicked. + this.element.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (this.text) { + // Open a new state with the interpolated contents. + CoreTextUtils.instance.viewText(this.plugin.name, this.text, { + component: this.component, + componentId: this.assign.cmid, + filter: true, + contextLevel: 'module', + instanceId: this.assign.cmid, + courseId: this.assign.course, + }); + } + }); + } else { + // Create and add the control. + this.control = this.fb.control(this.text); + } + + // Calculate initial words. + if (this.wordLimitEnabled) { + this.words = CoreTextUtils.instance.countWords(this.text); + } + } finally { + this.loaded = true; + } + } + + /** + * Text changed. + * + * @param text The new text. + */ + onChange(text: string): void { + // Count words if needed. + if (this.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 = window.setTimeout(() => { + this.words = CoreTextUtils.instance.countWords(text); + }, 1500); + } + } + +} diff --git a/src/addons/mod/assign/submission/onlinetext/lang.json b/src/addons/mod/assign/submission/onlinetext/lang.json new file mode 100644 index 000000000..e49362133 --- /dev/null +++ b/src/addons/mod/assign/submission/onlinetext/lang.json @@ -0,0 +1,4 @@ +{ + "pluginname": "Online text submissions", + "wordlimitexceeded": "The word limit for this assignment is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please review your submission and try again." +} \ No newline at end of file diff --git a/src/addons/mod/assign/submission/onlinetext/onlinetext.module.ts b/src/addons/mod/assign/submission/onlinetext/onlinetext.module.ts new file mode 100644 index 000000000..cbcaaa5ba --- /dev/null +++ b/src/addons/mod/assign/submission/onlinetext/onlinetext.module.ts @@ -0,0 +1,48 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { AddonModAssignSubmissionOnlineTextHandler } from './services/handler'; +import { AddonModAssignSubmissionOnlineTextComponent } from './component/onlinetext'; +import { CoreEditorComponentsModule } from '@features/editor/components/components.module'; +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate'; + +@NgModule({ + declarations: [ + AddonModAssignSubmissionOnlineTextComponent, + ], + imports: [ + CoreSharedModule, + CoreEditorComponentsModule, + ], + providers: [ + AddonModAssignSubmissionOnlineTextHandler, + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionOnlineTextHandler.instance); + }, + }, + ], + exports: [ + AddonModAssignSubmissionOnlineTextComponent, + ], + entryComponents: [ + AddonModAssignSubmissionOnlineTextComponent, + ], +}) +export class AddonModAssignSubmissionOnlineTextModule {} diff --git a/src/addons/mod/assign/submission/onlinetext/services/handler.ts b/src/addons/mod/assign/submission/onlinetext/services/handler.ts new file mode 100644 index 000000000..97696f2d5 --- /dev/null +++ b/src/addons/mod/assign/submission/onlinetext/services/handler.ts @@ -0,0 +1,323 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { + AddonModAssignAssign, + AddonModAssignSubmission, + AddonModAssignPlugin, + AddonModAssign, +} from '@addons/mod/assign/services/assign'; +import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper'; +import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } from '@addons/mod/assign/services/assign-offline'; +import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate'; +import { Injectable, Type } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { CoreFileHelper } from '@services/file-helper'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; +import { AddonModAssignSubmissionOnlineTextComponent } from '../component/onlinetext'; + +/** + * Handler for online text submission plugin. + */ +@Injectable( { providedIn: 'root' }) +export class AddonModAssignSubmissionOnlineTextHandlerService implements AddonModAssignSubmissionHandler { + + name = 'AddonModAssignSubmissionOnlineTextHandler'; + type = 'onlinetext'; + + /** + * 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. + * + * @return Boolean or promise resolved with boolean: whether it can be edited in offline. + */ + canEditOffline(): boolean { + // This plugin uses Moodle filters, it cannot be edited in offline. + return false; + } + + /** + * Check if a plugin has no data. + * + * @param assign The assignment. + * @param plugin The plugin object. + * @return Whether the plugin is empty. + */ + isEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean { + const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true); + + // If the text is empty, we can ignore files because they won't be visible anyways. + return text.trim().length === 0; + } + + /** + * 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 assign The assignment. + * @param plugin The plugin object. + * @param pluginData Object where to store the data to send. + * @param userId User ID. If not defined, site's current user. + * @param siteId Site ID. If not defined, current site. + * @return If the function is async, it should return a Promise resolved when done. + */ + async copySubmissionData( + assign: AddonModAssignAssign, + plugin: AddonModAssignPlugin, + pluginData: AddonModAssignSubmissionOnlineTextPluginData, + userId?: number, + siteId?: string, + ): Promise { + + const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true); + const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); + let itemId = 0; + + if (files.length) { + // Re-upload the files. + itemId = await AddonModAssignHelper.instance.uploadFiles(assign.id, files, siteId); + } + + 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. + * + * @return The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(): Type { + return AddonModAssignSubmissionOnlineTextComponent; + } + + /** + * Get files used by this plugin. + * The files returned by this function will be prefetched when the user prefetches the assign. + * + * @param assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @return The files (or promise resolved with the files). + */ + getPluginFiles( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + ): CoreWSExternalFile[] { + return AddonModAssign.instance.getSubmissionPluginAttachments(plugin); + } + + /** + * Get the size of data (in bytes) this plugin will send to copy a previous submission. + * + * @param assign The assignment. + * @param plugin The plugin object. + * @return The size (or promise resolved with size). + */ + async getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise { + const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true); + const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); + + const filesSize = await CoreFileHelper.instance.getTotalFilesSize(files); + + return text.length + filesSize; + } + + /** + * Get the size of data (in bytes) this plugin will send to add or edit a submission. + * + * @param assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @param inputData Data entered by the user for the submission. + * @return The size (or promise resolved with size). + */ + getSizeForEdit( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + ): number { + const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true); + + return text.length; + } + + /** + * Get the text to submit. + * + * @param plugin The plugin object. + * @param inputData Data entered by the user for the submission. + * @return Text to submit. + */ + protected getTextToSubmit(plugin: AddonModAssignPlugin, inputData: AddonModAssignSubmissionOnlineTextData): string { + const text = inputData.onlinetext_editor_text; + const files = plugin.fileareas && plugin.fileareas[0] && plugin.fileareas[0].files || []; + + return CoreTextUtils.instance.restorePluginfileUrls(text, files || []); + } + + /** + * Check if the submission data has changed for this plugin. + * + * @param assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @param inputData Data entered by the user for the submission. + * @return Boolean (or promise resolved with boolean): whether the data has changed. + */ + async hasDataChanged( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + inputData: AddonModAssignSubmissionOnlineTextData, + ): Promise { + + // Get the original text from plugin or offline. + const offlineData = + await CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getSubmission(assign.id, submission.userid)); + + let initialText = ''; + if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) { + initialText = (offlineData.plugindata).onlinetext_editor.text; + } else { + // No offline data found, get text from plugin. + initialText = plugin.editorfields && plugin.editorfields[0] ? plugin.editorfields[0].text : ''; + } + + // Check if text has changed. + return initialText != this.getTextToSubmit(plugin, inputData); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return True or promise resolved with true if enabled. + */ + async isEnabled(): Promise { + return true; + } + + /** + * Whether or not the handler is enabled for edit on a site level. + * + * @return Whether or not the handler is enabled for edit on a site level. + */ + isEnabledForEdit(): boolean { + // 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 = CoreSites.instance.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 assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @param inputData Data entered by the user for the submission. + * @param pluginData Object where to store the data to send. + * @param offline Whether the user is editing in offline. + * @param userId User ID. If not defined, site's current user. + * @param siteId Site ID. If not defined, current site. + * @return If the function is async, it should return a Promise resolved when done. + */ + prepareSubmissionData( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + inputData: AddonModAssignSubmissionOnlineTextData, + pluginData: AddonModAssignSubmissionOnlineTextPluginData, + ): void | Promise { + + let text = this.getTextToSubmit(plugin, inputData); + + // Check word limit. + const configs = AddonModAssignHelper.instance.getPluginConfig(assign, 'assignsubmission', plugin.type); + if (parseInt(configs.wordlimitenabled, 10)) { + const words = CoreTextUtils.instance.countWords(text); + const wordlimit = parseInt(configs.wordlimit, 10); + if (words > wordlimit) { + const params = { $a: { count: words, limit: wordlimit } }; + const message = Translate.instance.instant('addon.mod_assign_submission_onlinetext.wordlimitexceeded', params); + + throw new CoreError(message); + } + } + + // Add some HTML to the text if needed. + text = CoreTextUtils.instance.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 assign The assignment. + * @param submission The submission. + * @param plugin The plugin object. + * @param offlineData Offline data stored. + * @param pluginData Object where to store the data to send. + * @param siteId Site ID. If not defined, current site. + * @return If the function is async, it should return a Promise resolved when done. + */ + prepareSyncData( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + plugin: AddonModAssignPlugin, + offlineData: AddonModAssignSubmissionsDBRecordFormatted, + pluginData: AddonModAssignSubmissionOnlineTextPluginData, + ): void | Promise { + + const offlinePluginData = (offlineData && offlineData.plugindata); + const textData = offlinePluginData.onlinetext_editor; + if (textData) { + // Has some data to sync. + pluginData.onlinetext_editor = textData; + } + } + +} +export const AddonModAssignSubmissionOnlineTextHandler = makeSingleton(AddonModAssignSubmissionOnlineTextHandlerService); + +export type AddonModAssignSubmissionOnlineTextData = { + // The text for this submission. + onlinetext_editor_text: string; // eslint-disable-line @typescript-eslint/naming-convention +}; + +export type AddonModAssignSubmissionOnlineTextPluginData = { + // Editor structure. + onlinetext_editor: { // eslint-disable-line @typescript-eslint/naming-convention + text: string; // The text for this submission. + format: number; // The format for this submission. + itemid: number; // The draft area id for files attached to the submission. + }; +}; diff --git a/src/addons/mod/assign/submission/submission.module.ts b/src/addons/mod/assign/submission/submission.module.ts new file mode 100644 index 000000000..cb01e8e7c --- /dev/null +++ b/src/addons/mod/assign/submission/submission.module.ts @@ -0,0 +1,27 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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({ + imports: [ + AddonModAssignSubmissionCommentsModule, + AddonModAssignSubmissionFileModule, + AddonModAssignSubmissionOnlineTextModule, + ], +}) +export class AddonModAssignSubmissionModule { }