forked from CIT/Vmeda.Online
		
	MOBILE-2334 assign: Implement submission plugins
This commit is contained in:
		
							parent
							
								
									f3ae04600f
								
							
						
					
					
						commit
						cf927d4344
					
				| @ -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, | ||||
|  | ||||
							
								
								
									
										40
									
								
								src/addon/mod/assign/classes/submission-plugin-component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/addon/mod/assign/classes/submission-plugin-component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<any>} Promise resolved when done. | ||||
|      */ | ||||
|     invalidate(): Promise<any> { | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| } | ||||
| @ -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 | ||||
|  | ||||
| @ -0,0 +1,16 @@ | ||||
| 
 | ||||
| <core-dynamic-component [component]="pluginComponent" [data]="data"> | ||||
|     <!-- This content will be replaced by the component if found. --> | ||||
|     <core-loading [hideUntil]="pluginLoaded"> | ||||
|         <ion-item text-wrap *ngIf="text.length > 0 || files.length > 0"> | ||||
|             <h2>{{ plugin.name }}</h2> | ||||
|             <ion-badge *ngIf="notSupported" color="primary"> | ||||
|                 {{ 'addon.mod_assign.submissionnotsupported' | translate }} | ||||
|             </ion-badge> | ||||
|             <p *ngIf="text"> | ||||
|                 <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" [fullTitle]="plugin.name" [text]="text"></core-format-text> | ||||
|             </p> | ||||
|             <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-file> | ||||
|         </ion-item> | ||||
|     </core-loading> | ||||
| </core-dynamic-component> | ||||
| @ -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<any>} Promise resolved when done. | ||||
|      */ | ||||
|     invalidate(): Promise<any> { | ||||
|         return Promise.resolve(this.dynamicComponent && this.dynamicComponent.callComponentFunction('invalidate', [])); | ||||
|     } | ||||
| } | ||||
| @ -27,7 +27,7 @@ | ||||
|         <core-tab [title]="'addon.mod_assign.submission' | translate"> | ||||
|             <ng-template> | ||||
|                 <ion-content> | ||||
|                     <!-- @todo <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" assign="assign" submission="userSubmission" plugin="plugin" scroll-handle="{{scrollHandle}}"></addon-mod-assign-submission-plugin> --> | ||||
|                     <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign" [submission]="userSubmission" [plugin]="plugin"></addon-mod-assign-submission-plugin> | ||||
| 
 | ||||
|                     <!-- Render some data about the submission. --> | ||||
|                     <ion-item text-wrap *ngIf="userSubmission && userSubmission.status != statusNew && userSubmission.timemodified"> | ||||
|  | ||||
| @ -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<AddonModAssignSubmissionPluginComponent>; | ||||
| 
 | ||||
|     @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(() => { | ||||
|  | ||||
							
								
								
									
										48
									
								
								src/addon/mod/assign/submission/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/addon/mod/assign/submission/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,4 @@ | ||||
| <ion-item text-wrap (click)="showComments()"> | ||||
|     <h2>{{plugin.name}}</h2> | ||||
|     <core-comments contextLevel="module" [instanceId]="assign.cmid" component="assignsubmission_comments" [itemId]="submission.id" area="submission_comments" [title]="plugin.name"></core-comments> | ||||
| </ion-item> | ||||
| @ -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<any>} Promise resolved when done. | ||||
|      */ | ||||
|     invalidate(): Promise<any> { | ||||
|         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(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/addon/mod/assign/submission/comments/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addon/mod/assign/submission/comments/lang/en.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| { | ||||
|     "pluginname": "Submission comments" | ||||
| } | ||||
| @ -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>} Boolean or promise resolved with boolean: whether it can be edited in offline. | ||||
|      */ | ||||
|     canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise<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 {Injector} injector Injector. | ||||
|      * @param {any} plugin The plugin object. | ||||
|      * @param {boolean} [edit] Whether the user is editing. | ||||
|      * @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise<any> { | ||||
|         return edit ? undefined : AddonModAssignSubmissionCommentsComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled for edit on a site level. | ||||
|      * | ||||
|      * @return {boolean|Promise<boolean>} Whether or not the handler is enabled for edit on a site level. | ||||
|      */ | ||||
|     isEnabledForEdit(): boolean | Promise<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 {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<any>} Promise resolved when done. | ||||
|      */ | ||||
|     prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise<any> { | ||||
|         return this.commentsProvider.getComments('module', assign.cmid, 'assignsubmission_comments', submission.id, | ||||
|                 'submission_comments', 0, siteId).catch(() => { | ||||
|             // Fail silently (Moodle < 3.1.1, 3.2)
 | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/addon/mod/assign/submission/file/component/file.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/addon/mod/assign/submission/file/component/file.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| <!-- Read only. --> | ||||
| <ion-item text-wrap *ngIf="files && files.length && !edit"> | ||||
|     <h2>{{plugin.name}}</h2> | ||||
|     <div *ngFor="let file of files" no-lines> | ||||
|         <!-- Files already attached to the submission. --> | ||||
|         <core-file *ngIf="!file.name" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-file> | ||||
| 
 | ||||
|         <!-- Files stored in offline to be sent later. --> | ||||
|         <core-local-file *ngIf="file.name" [file]="file"></core-local-file> | ||||
|     </div> | ||||
| </ion-item> | ||||
| 
 | ||||
| <!-- Edit --> | ||||
| <div *ngIf="edit"> | ||||
|     <ion-item-divider text-wrap color="light">{{plugin.name}}</ion-item-divider> | ||||
|     <core-attachments [files]="files" [maxSize]="configs.maxsubmissionsizebytes" [maxSubmissions]="configs.maxfilesubmissions" [component]="component" [componentId]="assign.cmid" [acceptedTypes]="configs.filetypeslist" [allowOffline]="allowOffline"></core-attachments> | ||||
| </div> | ||||
							
								
								
									
										74
									
								
								src/addon/mod/assign/submission/file/component/file.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/addon/mod/assign/submission/file/component/file.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								src/addon/mod/assign/submission/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/addon/mod/assign/submission/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { 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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/addon/mod/assign/submission/file/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addon/mod/assign/submission/file/lang/en.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| { | ||||
|     "pluginname": "File submissions" | ||||
| } | ||||
							
								
								
									
										361
									
								
								src/addon/mod/assign/submission/file/providers/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										361
									
								
								src/addon/mod/assign/submission/file/providers/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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>} Boolean or promise resolved with boolean: whether it can be edited in offline. | ||||
|      */ | ||||
|     canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise<boolean> { | ||||
|         // 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<any>} 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<any> { | ||||
|         // 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<any>} The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise<any> { | ||||
|         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<any>} 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<any> { | ||||
|         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<any[]>} The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]> { | ||||
|         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<number>} The size (or promise resolved with size). | ||||
|      */ | ||||
|     getSizeForCopy(assign: any, plugin: any): number | Promise<number> { | ||||
|         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<number>} The size (or promise resolved with size). | ||||
|      */ | ||||
|     getSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): number | Promise<number> { | ||||
|         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>} Boolean (or promise resolved with boolean): whether the data has changed. | ||||
|      */ | ||||
|     hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise<boolean> { | ||||
|         // 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<boolean>} True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled for edit on a site level. | ||||
|      * | ||||
|      * @return {boolean|Promise<boolean>} Whether or not the handler is enabled for edit on a site level. | ||||
|      */ | ||||
|     isEnabledForEdit(): boolean | Promise<boolean> { | ||||
|         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<any>} 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<any> { | ||||
| 
 | ||||
|         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<any>} 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<any> { | ||||
| 
 | ||||
|         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; | ||||
|                 }); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,21 @@ | ||||
| <!-- Read only --> | ||||
| <ion-item text-wrap *ngIf="!edit && text"> | ||||
|     <h2>{{ plugin.name }}</h2> | ||||
|     <p *ngIf="words">{{ 'addon.mod_assign.numwords' | translate: {'$a': words} }}</p> | ||||
|     <p> | ||||
|         <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" [fullTitle]="plugin.name" [text]="text"></core-format-text> | ||||
|     </p> | ||||
| </ion-item> | ||||
| 
 | ||||
| <!-- Edit --> | ||||
| <div *ngIf="edit && loaded"> | ||||
|     <ion-item-divider text-wrap color="light">{{ plugin.name }}</ion-item-divider> | ||||
|     <ion-item text-wrap *ngIf="configs.wordlimitenabled && words >= 0"> | ||||
|         <h2>{{ 'addon.mod_assign.wordlimit' | translate }}</h2> | ||||
|         <p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}</p> | ||||
|     </ion-item> | ||||
|     <ion-item text-wrap> | ||||
|         <!-- @todo: [component]="component" [componentId]="assign.cmid" --> | ||||
|         <core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="onlinetext_editor_text" (contentChanged)="onChange($event)"></core-rich-text-editor> | ||||
|     </ion-item> | ||||
| </div> | ||||
| @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/addon/mod/assign/submission/onlinetext/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addon/mod/assign/submission/onlinetext/lang/en.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| { | ||||
|     "pluginname": "Online text submissions" | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										277
									
								
								src/addon/mod/assign/submission/onlinetext/providers/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								src/addon/mod/assign/submission/onlinetext/providers/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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>} Boolean or promise resolved with boolean: whether it can be edited in offline. | ||||
|      */ | ||||
|     canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise<boolean> { | ||||
|         // 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<any>} 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<any> { | ||||
|         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<any>} The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise<any> { | ||||
|         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<any[]>} The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]> { | ||||
|         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<number>} The size (or promise resolved with size). | ||||
|      */ | ||||
|     getSizeForCopy(assign: any, plugin: any): number | Promise<number> { | ||||
|         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<number>} The size (or promise resolved with size). | ||||
|      */ | ||||
|     getSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): number | Promise<number> { | ||||
|         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>} Boolean (or promise resolved with boolean): whether the data has changed. | ||||
|      */ | ||||
|     hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise<boolean> { | ||||
|         // 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<boolean>} True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled for edit on a site level. | ||||
|      * | ||||
|      * @return {boolean|Promise<boolean>} Whether or not the handler is enabled for edit on a site level. | ||||
|      */ | ||||
|     isEnabledForEdit(): boolean | Promise<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 = 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<any>} 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<any> { | ||||
| 
 | ||||
|         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<any>} 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<any> { | ||||
| 
 | ||||
|         const textData = offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor; | ||||
|         if (textData) { | ||||
|             // Has some data to sync.
 | ||||
|             pluginData.onlinetext_editor = textData; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/addon/mod/assign/submission/submission.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/addon/mod/assign/submission/submission.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 { } | ||||
| @ -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 { | ||||
|  | ||||
							
								
								
									
										25
									
								
								src/components/attachments/attachments.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/components/attachments/attachments.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| <ion-item text-wrap> | ||||
|     {{ 'core.maxsizeandattachments' | translate:{$a: {size: maxSizeReadable, attachments: maxSubmissionsReadable} } }} | ||||
| </ion-item> | ||||
| <ion-item text-wrap *ngIf="filetypes && filetypes.mimetypes && filetypes.mimetypes.length"> | ||||
|     <p>{{ 'core.fileuploader.filesofthesetypes' | translate }}</p> | ||||
|     <ul class="list-with-style"> | ||||
|         <li *ngFor="let typeInfo of filetypes.info"> | ||||
|             <strong *ngIf="typeInfo.name">{{typeInfo.name}} </strong>{{typeInfo.extlist}} | ||||
|         </li> | ||||
|     </ul> | ||||
| </ion-item> | ||||
| <div *ngFor="let file of files; let index=index"> | ||||
|     <!-- Files already attached to the submission, either in online or in offline. --> | ||||
|     <core-file *ngIf="!file.name || file.offline" [file]="file" [component]="component" [componentId]="componentId" [canDelete]="true" (onDelete)="delete(index, true)" [canDownload]="!file.offline"></core-file> | ||||
| 
 | ||||
|     <!-- Files added to draft but not attached to submission yet. --> | ||||
|     <core-local-file *ngIf="file.name && !file.offline" [file]="file" [manage]="true" (onDelete)="delete(index, false)" (onRename)="renamed(index, $event)"></core-local-file> | ||||
| </div> | ||||
| <!-- Button to add more files. --> | ||||
| <ion-item text-wrap *ngIf="unlimitedFiles || (maxSubmissions >= 0 && files && files.length < maxSubmissions)"> | ||||
|     <a ion-button block icon-start (click)="add()"> | ||||
|         <ion-icon name="add"></ion-icon> | ||||
|         {{ 'core.fileuploader.addfiletext' | translate }} | ||||
|     </a> | ||||
| </ion-item> | ||||
							
								
								
									
										135
									
								
								src/components/attachments/attachments.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/components/attachments/attachments.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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. | ||||
|  * | ||||
|  * <core-attachments [files]="files" [maxSize]="configs.maxsubmissionsizebytes" [maxSubmissions]="configs.maxfilesubmissions" | ||||
|  *     [component]="component" [componentId]="assign.cmid" [acceptedTypes]="configs.filetypeslist" [allowOffline]="allowOffline"> | ||||
|  * </core-attachments> | ||||
|  */ | ||||
| @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; | ||||
|     } | ||||
| } | ||||
| @ -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 {} | ||||
|  | ||||
| @ -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<string>; // Will notify when the delete button is clicked.
 | ||||
|     @Output() onDelete?: EventEmitter<void>; // 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(); | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -487,7 +487,7 @@ export class CoreFileUploaderProvider { | ||||
|      * @return {Promise<number>} Promise resolved with the itemId. | ||||
|      */ | ||||
|     uploadOrReuploadFiles(files: any[], component?: string, componentId?: string | number, siteId?: string): Promise<number> { | ||||
|             siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (!files || !files.length) { | ||||
|             // Return fake draft ID.
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user