MOBILE-2334 assign: Implement submission plugins
parent
f3ae04600f
commit
cf927d4344
|
@ -27,10 +27,14 @@ import { AddonModAssignDefaultSubmissionHandler } from './providers/default-subm
|
||||||
import { AddonModAssignModuleHandler } from './providers/module-handler';
|
import { AddonModAssignModuleHandler } from './providers/module-handler';
|
||||||
import { AddonModAssignPrefetchHandler } from './providers/prefetch-handler';
|
import { AddonModAssignPrefetchHandler } from './providers/prefetch-handler';
|
||||||
import { AddonModAssignSyncCronHandler } from './providers/sync-cron-handler';
|
import { AddonModAssignSyncCronHandler } from './providers/sync-cron-handler';
|
||||||
|
import { AddonModAssignSubmissionModule } from './submission/submission.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
],
|
],
|
||||||
|
imports: [
|
||||||
|
AddonModAssignSubmissionModule
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AddonModAssignProvider,
|
AddonModAssignProvider,
|
||||||
AddonModAssignOfflineProvider,
|
AddonModAssignOfflineProvider,
|
||||||
|
|
|
@ -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 { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||||
import { AddonModAssignIndexComponent } from './index/index';
|
import { AddonModAssignIndexComponent } from './index/index';
|
||||||
import { AddonModAssignSubmissionComponent } from './submission/submission';
|
import { AddonModAssignSubmissionComponent } from './submission/submission';
|
||||||
|
import { AddonModAssignSubmissionPluginComponent } from './submission-plugin/submission-plugin';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AddonModAssignIndexComponent,
|
AddonModAssignIndexComponent,
|
||||||
AddonModAssignSubmissionComponent
|
AddonModAssignSubmissionComponent,
|
||||||
|
AddonModAssignSubmissionPluginComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -41,7 +43,8 @@ import { AddonModAssignSubmissionComponent } from './submission/submission';
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AddonModAssignIndexComponent,
|
AddonModAssignIndexComponent,
|
||||||
AddonModAssignSubmissionComponent
|
AddonModAssignSubmissionComponent,
|
||||||
|
AddonModAssignSubmissionPluginComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
AddonModAssignIndexComponent
|
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">
|
<core-tab [title]="'addon.mod_assign.submission' | translate">
|
||||||
<ng-template>
|
<ng-template>
|
||||||
<ion-content>
|
<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. -->
|
<!-- Render some data about the submission. -->
|
||||||
<ion-item text-wrap *ngIf="userSubmission && userSubmission.status != statusNew && userSubmission.timemodified">
|
<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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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 { NavController } from 'ionic-angular';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CoreAppProvider } from '@providers/app';
|
import { CoreAppProvider } from '@providers/app';
|
||||||
|
@ -35,6 +35,7 @@ import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { CoreTabsComponent } from '@components/tabs/tabs';
|
import { CoreTabsComponent } from '@components/tabs/tabs';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays an assignment submission.
|
* Component that displays an assignment submission.
|
||||||
|
@ -45,6 +46,7 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
})
|
})
|
||||||
export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild(CoreTabsComponent) tabs: CoreTabsComponent;
|
@ViewChild(CoreTabsComponent) tabs: CoreTabsComponent;
|
||||||
|
@ViewChildren(AddonModAssignSubmissionPluginComponent) submissionComponents: QueryList<AddonModAssignSubmissionPluginComponent>;
|
||||||
|
|
||||||
@Input() courseId: number; // Course ID the submission belongs to.
|
@Input() courseId: number; // Course ID the submission belongs to.
|
||||||
@Input() moduleId: number; // Module 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.gradesHelper.invalidateGradeModuleItems(this.courseId, this.submitId));
|
||||||
promises.push(this.courseProvider.invalidateModule(this.moduleId));
|
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(() => {
|
return Promise.all(promises).catch(() => {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"pluginname": "File submissions"
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
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 {
|
.core-oauth-icon, .item.core-oauth-icon, .list .item.core-oauth-icon {
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
img, .label {
|
img, .label {
|
||||||
|
|
|
@ -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>
|
|
@ -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 { CoreTimerComponent } from './timer/timer';
|
||||||
import { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha/recaptcha';
|
import { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha/recaptcha';
|
||||||
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
||||||
|
import { CoreAttachmentsComponent } from './attachments/attachments';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -72,7 +73,8 @@ import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
||||||
CoreTimerComponent,
|
CoreTimerComponent,
|
||||||
CoreRecaptchaComponent,
|
CoreRecaptchaComponent,
|
||||||
CoreRecaptchaModalComponent,
|
CoreRecaptchaModalComponent,
|
||||||
CoreNavigationBarComponent
|
CoreNavigationBarComponent,
|
||||||
|
CoreAttachmentsComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
CoreContextMenuPopoverComponent,
|
CoreContextMenuPopoverComponent,
|
||||||
|
@ -109,7 +111,8 @@ import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
||||||
CoreSendMessageFormComponent,
|
CoreSendMessageFormComponent,
|
||||||
CoreTimerComponent,
|
CoreTimerComponent,
|
||||||
CoreRecaptchaComponent,
|
CoreRecaptchaComponent,
|
||||||
CoreNavigationBarComponent
|
CoreNavigationBarComponent,
|
||||||
|
CoreAttachmentsComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
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.
|
@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.
|
// Use it for files that you cannot determine if they're outdated or not.
|
||||||
@Input() canDownload?: boolean | string = true; // Whether file can be downloaded.
|
@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;
|
isDownloaded: boolean;
|
||||||
isDownloading: boolean;
|
isDownloading: boolean;
|
||||||
|
@ -178,7 +178,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
|
||||||
*
|
*
|
||||||
* @param {Event} e Click event.
|
* @param {Event} e Click event.
|
||||||
*/
|
*/
|
||||||
deleteFile(e: Event): void {
|
delete(e: Event): void {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
|
|
@ -177,8 +177,8 @@ export class CoreLocalFileComponent implements OnInit {
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
});
|
});
|
||||||
}).catch(() => {
|
}).catch((error) => {
|
||||||
this.domUtils.showErrorModal('core.errordeletefile', true);
|
this.domUtils.showErrorModalDefault(error, 'core.errordeletefile', true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue