MOBILE-2334 assign: Implement submission plugins

main
Dani Palou 2018-04-13 15:32:06 +02:00
parent f3ae04600f
commit cf927d4344
30 changed files with 1564 additions and 11 deletions

View File

@ -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,

View 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();
}
}

View File

@ -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

View File

@ -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>

View File

@ -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', []));
}
}

View File

@ -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">

View File

@ -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(() => {

View 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);
}
}

View File

@ -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>

View 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 { 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();
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "Submission comments"
}

View File

@ -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)
});
}
}

View 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>

View 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);
});
}
}

View 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);
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "File submissions"
}

View 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;
});
});
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "Online text submissions"
}

View 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 { 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);
}
}

View 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;
}
}
}

View 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 { }

View File

@ -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 {

View 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>

View 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;
}
}

View 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 {}

View File

@ -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();

View File

@ -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);
});
}
}

View File

@ -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.