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 { 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,
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Input } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Base class for component to render a submission plugin.
|
||||
*/
|
||||
export class AddonModAssignSubmissionPluginComponent {
|
||||
@Input() assign: any; // The assignment.
|
||||
@Input() submission: any; // The submission.
|
||||
@Input() plugin: any; // The plugin object.
|
||||
@Input() configs: any; // The configs for the plugin.
|
||||
@Input() edit: boolean; // Whether the user is editing.
|
||||
@Input() allowOffline: boolean; // Whether to allow offline.
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
invalidate(): Promise<any> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
|
@ -22,11 +22,13 @@ import { CorePipesModule } from '@pipes/pipes.module';
|
|||
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||
import { AddonModAssignIndexComponent } from './index/index';
|
||||
import { AddonModAssignSubmissionComponent } from './submission/submission';
|
||||
import { AddonModAssignSubmissionPluginComponent } from './submission-plugin/submission-plugin';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignIndexComponent,
|
||||
AddonModAssignSubmissionComponent
|
||||
AddonModAssignSubmissionComponent,
|
||||
AddonModAssignSubmissionPluginComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -41,7 +43,8 @@ import { AddonModAssignSubmissionComponent } from './submission/submission';
|
|||
],
|
||||
exports: [
|
||||
AddonModAssignIndexComponent,
|
||||
AddonModAssignSubmissionComponent
|
||||
AddonModAssignSubmissionComponent,
|
||||
AddonModAssignSubmissionPluginComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignIndexComponent
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
<core-dynamic-component [component]="pluginComponent" [data]="data">
|
||||
<!-- This content will be replaced by the component if found. -->
|
||||
<core-loading [hideUntil]="pluginLoaded">
|
||||
<ion-item text-wrap *ngIf="text.length > 0 || files.length > 0">
|
||||
<h2>{{ plugin.name }}</h2>
|
||||
<ion-badge *ngIf="notSupported" color="primary">
|
||||
{{ 'addon.mod_assign.submissionnotsupported' | translate }}
|
||||
</ion-badge>
|
||||
<p *ngIf="text">
|
||||
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" [fullTitle]="plugin.name" [text]="text"></core-format-text>
|
||||
</p>
|
||||
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-file>
|
||||
</ion-item>
|
||||
</core-loading>
|
||||
</core-dynamic-component>
|
|
@ -0,0 +1,98 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit, Injector, ViewChild } from '@angular/core';
|
||||
import { AddonModAssignProvider } from '../../providers/assign';
|
||||
import { AddonModAssignHelperProvider } from '../../providers/helper';
|
||||
import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate';
|
||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
|
||||
/**
|
||||
* Component that displays an assignment submission plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-submission-plugin',
|
||||
templateUrl: 'submission-plugin.html',
|
||||
})
|
||||
export class AddonModAssignSubmissionPluginComponent implements OnInit {
|
||||
@ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent;
|
||||
|
||||
@Input() assign: any; // The assignment.
|
||||
@Input() submission: any; // The submission.
|
||||
@Input() plugin: any; // The plugin object.
|
||||
@Input() edit: boolean | string; // Whether the user is editing.
|
||||
@Input() allowOffline: boolean | string; // Whether to allow offline.
|
||||
|
||||
pluginComponent: any; // Component to render the plugin.
|
||||
data: any; // Data to pass to the component.
|
||||
|
||||
// Data to render the plugin if it isn't supported.
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
text = '';
|
||||
files = [];
|
||||
notSupported: boolean;
|
||||
pluginLoaded: boolean;
|
||||
|
||||
constructor(protected injector: Injector, protected submissionDelegate: AddonModAssignSubmissionDelegate,
|
||||
protected assignProvider: AddonModAssignProvider, protected assignHelper: AddonModAssignHelperProvider) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (!this.plugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.plugin.name = this.submissionDelegate.getPluginName(this.plugin);
|
||||
if (!this.plugin.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.edit = this.edit && this.edit !== 'false';
|
||||
this.allowOffline = this.allowOffline && this.allowOffline !== 'false';
|
||||
|
||||
// Check if the plugin has defined its own component to render itself.
|
||||
this.submissionDelegate.getComponentForPlugin(this.injector, this.plugin, this.edit).then((component) => {
|
||||
this.pluginComponent = component;
|
||||
|
||||
if (component) {
|
||||
// Prepare the data to pass to the component.
|
||||
this.data = {
|
||||
assign: this.assign,
|
||||
submission: this.submission,
|
||||
plugin: this.plugin,
|
||||
configs: this.assignHelper.getPluginConfig(this.assign, 'assignsubmission', this.plugin.type),
|
||||
edit: this.edit,
|
||||
allowOffline: this.allowOffline
|
||||
};
|
||||
} else {
|
||||
// Data to render the plugin.
|
||||
this.text = this.assignProvider.getSubmissionPluginText(this.plugin);
|
||||
this.files = this.assignProvider.getSubmissionPluginAttachments(this.plugin);
|
||||
this.notSupported = this.submissionDelegate.isPluginSupported(this.plugin.type);
|
||||
this.pluginLoaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the plugin data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
invalidate(): Promise<any> {
|
||||
return Promise.resolve(this.dynamicComponent && this.dynamicComponent.callComponentFunction('invalidate', []));
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@
|
|||
<core-tab [title]="'addon.mod_assign.submission' | translate">
|
||||
<ng-template>
|
||||
<ion-content>
|
||||
<!-- @todo <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" assign="assign" submission="userSubmission" plugin="plugin" scroll-handle="{{scrollHandle}}"></addon-mod-assign-submission-plugin> -->
|
||||
<addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign" [submission]="userSubmission" [plugin]="plugin"></addon-mod-assign-submission-plugin>
|
||||
|
||||
<!-- Render some data about the submission. -->
|
||||
<ion-item text-wrap *ngIf="userSubmission && userSubmission.status != statusNew && userSubmission.timemodified">
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit, OnDestroy, ViewChild, Optional } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnDestroy, ViewChild, Optional, ViewChildren, QueryList } from '@angular/core';
|
||||
import { NavController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
|
@ -35,6 +35,7 @@ import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
|
|||
import * as moment from 'moment';
|
||||
import { CoreTabsComponent } from '@components/tabs/tabs';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin';
|
||||
|
||||
/**
|
||||
* Component that displays an assignment submission.
|
||||
|
@ -45,6 +46,7 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
|||
})
|
||||
export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(CoreTabsComponent) tabs: CoreTabsComponent;
|
||||
@ViewChildren(AddonModAssignSubmissionPluginComponent) submissionComponents: QueryList<AddonModAssignSubmissionPluginComponent>;
|
||||
|
||||
@Input() courseId: number; // Course ID the submission belongs to.
|
||||
@Input() moduleId: number; // Module ID the submission belongs to.
|
||||
|
@ -328,6 +330,13 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
promises.push(this.gradesHelper.invalidateGradeModuleItems(this.courseId, this.submitId));
|
||||
promises.push(this.courseProvider.invalidateModule(this.moduleId));
|
||||
|
||||
// Invalidate plugins.
|
||||
if (this.submissionComponents && this.submissionComponents.length) {
|
||||
this.submissionComponents.forEach((component) => {
|
||||
promises.push(component.invalidate());
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all(promises).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -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 { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha/recaptcha';
|
||||
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
||||
import { CoreAttachmentsComponent } from './attachments/attachments';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -72,7 +73,8 @@ import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
|||
CoreTimerComponent,
|
||||
CoreRecaptchaComponent,
|
||||
CoreRecaptchaModalComponent,
|
||||
CoreNavigationBarComponent
|
||||
CoreNavigationBarComponent,
|
||||
CoreAttachmentsComponent
|
||||
],
|
||||
entryComponents: [
|
||||
CoreContextMenuPopoverComponent,
|
||||
|
@ -109,7 +111,8 @@ import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
|||
CoreSendMessageFormComponent,
|
||||
CoreTimerComponent,
|
||||
CoreRecaptchaComponent,
|
||||
CoreNavigationBarComponent
|
||||
CoreNavigationBarComponent,
|
||||
CoreAttachmentsComponent
|
||||
]
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -39,7 +39,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
|
|||
@Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded.
|
||||
// Use it for files that you cannot determine if they're outdated or not.
|
||||
@Input() canDownload?: boolean | string = true; // Whether file can be downloaded.
|
||||
@Output() onDelete?: EventEmitter<string>; // Will notify when the delete button is clicked.
|
||||
@Output() onDelete?: EventEmitter<void>; // Will notify when the delete button is clicked.
|
||||
|
||||
isDownloaded: boolean;
|
||||
isDownloading: boolean;
|
||||
|
@ -178,7 +178,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
|
|||
*
|
||||
* @param {Event} e Click event.
|
||||
*/
|
||||
deleteFile(e: Event): void {
|
||||
delete(e: Event): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
|
|
@ -177,8 +177,8 @@ export class CoreLocalFileComponent implements OnInit {
|
|||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch(() => {
|
||||
this.domUtils.showErrorModal('core.errordeletefile', true);
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.errordeletefile', true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue