MOBILE-3636 assign: Submission plugins

main
Pau Ferrer Ocaña 2021-02-12 16:43:59 +01:00
parent 3281196ec0
commit 85f79bb944
23 changed files with 1371 additions and 32 deletions

View File

@ -29,6 +29,7 @@ import { AddonModAssignModuleHandler, AddonModAssignModuleHandlerService } from
import { AddonModAssignPrefetchHandler } from './services/handlers/prefetch'; import { AddonModAssignPrefetchHandler } from './services/handlers/prefetch';
import { AddonModAssignPushClickHandler } from './services/handlers/push-click'; import { AddonModAssignPushClickHandler } from './services/handlers/push-click';
import { AddonModAssignSyncCronHandler } from './services/handlers/sync-cron'; import { AddonModAssignSyncCronHandler } from './services/handlers/sync-cron';
import { AddonModAssignSubmissionModule } from './submission/submission.module';
const routes: Routes = [ const routes: Routes = [
{ {
@ -41,6 +42,7 @@ const routes: Routes = [
imports: [ imports: [
CoreMainMenuTabRoutingModule.forChild(routes), CoreMainMenuTabRoutingModule.forChild(routes),
AddonModAssignComponentsModule, AddonModAssignComponentsModule,
AddonModAssignSubmissionModule,
], ],
providers: [ providers: [
{ {

View File

@ -24,6 +24,7 @@ import {
} from '../../services/assign'; } from '../../services/assign';
import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper'; import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper';
import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate'; import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate';
import { FileEntry } from '@ionic-native/file/ngx';
/** /**
* Component that displays an assignment submission plugin. * Component that displays an assignment submission plugin.
@ -48,7 +49,7 @@ export class AddonModAssignSubmissionPluginComponent implements OnInit {
// Data to render the plugin if it isn't supported. // Data to render the plugin if it isn't supported.
component = AddonModAssignProvider.COMPONENT; component = AddonModAssignProvider.COMPONENT;
text = ''; text = '';
files: CoreWSExternalFile[] = []; files: (FileEntry | CoreWSExternalFile)[] = [];
notSupported = false; notSupported = false;
pluginLoaded = false; pluginLoaded = false;

View File

@ -392,7 +392,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
try { try {
return AddonModAssignHelper.instance.hasFeedbackDataChanged( return AddonModAssignHelper.instance.hasFeedbackDataChanged(
this.assign!, this.assign!,
this.userSubmission, this.userSubmission!, // @todo
this.feedback, this.feedback,
this.submitId, this.submitId,
); );

View File

@ -492,7 +492,7 @@ export class AddonModAssignHelperProvider {
*/ */
async hasFeedbackDataChanged( async hasFeedbackDataChanged(
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted,
feedback: AddonModAssignSubmissionFeedback, feedback: AddonModAssignSubmissionFeedback,
userId: number, userId: number,
): Promise<boolean> { ): Promise<boolean> {
@ -683,15 +683,13 @@ export class AddonModAssignHelperProvider {
offline = false, offline = false,
userId?: number, userId?: number,
siteId?: string, siteId?: string,
): Promise<void> { ): Promise<number | CoreFileUploaderStoreFilesResult> {
if (offline) { if (offline) {
await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId); return await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
return;
} }
await this.uploadFiles(assignId, files, siteId); return await this.uploadFiles(assignId, files, siteId);
} }
} }

View File

@ -29,6 +29,7 @@ import { CoreApp } from '@services/app';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { AddonModAssignOffline } from './assign-offline'; import { AddonModAssignOffline } from './assign-offline';
import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignSubmissionDelegate } from './submission-delegate';
import { CoreComments } from '@features/comments/services/comments';
const ROOT_CACHE_KEY = 'mmaModAssign:'; const ROOT_CACHE_KEY = 'mmaModAssign:';
@ -754,7 +755,7 @@ export class AddonModAssignProvider {
promises.push(this.invalidateAssignmentUserMappingsData(assign.id, siteId)); promises.push(this.invalidateAssignmentUserMappingsData(assign.id, siteId));
promises.push(this.invalidateAssignmentGradesData(assign.id, siteId)); promises.push(this.invalidateAssignmentGradesData(assign.id, siteId));
promises.push(this.invalidateListParticipantsData(assign.id, siteId)); promises.push(this.invalidateListParticipantsData(assign.id, siteId));
// @todo promises.push(CoreComments.instance.invalidateCommentsByInstance('module', assign.id, siteId)); promises.push(CoreComments.instance.invalidateCommentsByInstance('module', assign.id, siteId));
promises.push(this.invalidateAssignmentData(courseId, siteId)); promises.push(this.invalidateAssignmentData(courseId, siteId));
promises.push(CoreGrades.instance.invalidateAllCourseGradesData(courseId)); promises.push(CoreGrades.instance.invalidateAllCourseGradesData(courseId));

View File

@ -18,6 +18,7 @@ import { AddonModAssignDefaultFeedbackHandler } from './handlers/default-feedbac
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign'; import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreWSExternalFile } from '@services/ws'; import { CoreWSExternalFile } from '@services/ws';
import { AddonModAssignSubmissionFormatted } from './assign-helper';
/** /**
* Interface that all feedback handlers must implement. * Interface that all feedback handlers must implement.
@ -264,7 +265,7 @@ export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonMod
*/ */
async hasPluginDataChanged( async hasPluginDataChanged(
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
inputData: any, inputData: any,
userId: number, userId: number,

View File

@ -18,6 +18,7 @@ import { AddonModAssignDefaultSubmissionHandler } from './handlers/default-submi
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign'; import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreWSExternalFile } from '@services/ws'; import { CoreWSExternalFile } from '@services/ws';
import { AddonModAssignSubmissionsDBRecordFormatted } from './assign-offline';
/** /**
* Interface that all submission handlers must implement. * Interface that all submission handlers must implement.
@ -69,7 +70,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
inputData: any, inputData: Record<string, unknown>,
): void; ): void;
/** /**
@ -105,9 +106,9 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
offlineData: any, offlineData: AddonModAssignSubmissionsDBRecordFormatted,
siteId?: string, siteId?: string,
): void | Promise<any>; ): void | Promise<void>;
/** /**
* Return the Component to use to display the plugin data, either in read or in edit mode. * Return the Component to use to display the plugin data, either in read or in edit mode.
@ -172,7 +173,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
inputData: any, inputData: Record<string, unknown>,
): number | Promise<number>; ): number | Promise<number>;
/** /**
@ -188,7 +189,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
inputData: any, inputData: Record<string, unknown>,
): boolean | Promise<boolean>; ): boolean | Promise<boolean>;
/** /**
@ -232,12 +233,12 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
inputData: any, inputData: Record<string, unknown>,
pluginData: AddonModAssignSavePluginData, pluginData: AddonModAssignSavePluginData,
offline?: boolean, offline?: boolean,
userId?: number, userId?: number,
siteId?: string, siteId?: string,
): void | Promise<any>; ): void | Promise<void>;
/** /**
* Prepare and add to pluginData the data to send to the server based on the offline data stored. * Prepare and add to pluginData the data to send to the server based on the offline data stored.
@ -255,10 +256,10 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
offlineData: any, offlineData: AddonModAssignSubmissionsDBRecordFormatted,
pluginData: any, pluginData: AddonModAssignSavePluginData,
siteId?: string, siteId?: string,
): void | Promise<any>; ): void | Promise<void>;
} }
/** /**
@ -303,7 +304,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
inputData: any, inputData: Record<string, unknown>,
): void { ): void {
return this.executeFunctionOnEnabled(plugin.type, 'clearTmpData', [assign, submission, plugin, inputData]); return this.executeFunctionOnEnabled(plugin.type, 'clearTmpData', [assign, submission, plugin, inputData]);
} }
@ -346,9 +347,9 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
offlineData: any, offlineData: AddonModAssignSubmissionsDBRecordFormatted,
siteId?: string, siteId?: string,
): Promise<any | undefined> { ): Promise<void> {
return await this.executeFunctionOnEnabled( return await this.executeFunctionOnEnabled(
plugin.type, plugin.type,
'deleteOfflineData', 'deleteOfflineData',
@ -423,7 +424,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
inputData: any, inputData: Record<string, unknown>,
): Promise<number | undefined> { ): Promise<number | undefined> {
return await this.executeFunctionOnEnabled( return await this.executeFunctionOnEnabled(
plugin.type, plugin.type,
@ -445,7 +446,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
inputData: any, inputData: Record<string, unknown>,
): Promise<boolean | undefined> { ): Promise<boolean | undefined> {
return await this.executeFunctionOnEnabled( return await this.executeFunctionOnEnabled(
plugin.type, plugin.type,
@ -520,12 +521,12 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
inputData: any, inputData: Record<string, unknown>,
pluginData: any, pluginData: AddonModAssignSavePluginData,
offline?: boolean, offline?: boolean,
userId?: number, userId?: number,
siteId?: string, siteId?: string,
): Promise<any | undefined> { ): Promise<void | undefined> {
return await this.executeFunctionOnEnabled( return await this.executeFunctionOnEnabled(
plugin.type, plugin.type,
@ -549,10 +550,10 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin, plugin: AddonModAssignPlugin,
offlineData: any, offlineData: AddonModAssignSubmissionsDBRecordFormatted,
pluginData: any, pluginData: AddonModAssignSavePluginData,
siteId?: string, siteId?: string,
): Promise<any | undefined> { ): Promise<void> {
return this.executeFunctionOnEnabled( return this.executeFunctionOnEnabled(
plugin.type, plugin.type,
@ -562,4 +563,4 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
} }
} }
export class AddonModAssignSubmissionDelegate extends makeSingleton(AddonModAssignSubmissionDelegateService) {} export const AddonModAssignSubmissionDelegate = makeSingleton(AddonModAssignSubmissionDelegateService);

View File

@ -0,0 +1,48 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonModAssignSubmissionCommentsHandler } from './services/handler';
import { AddonModAssignSubmissionCommentsComponent } from './component/comments';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate';
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
@NgModule({
declarations: [
AddonModAssignSubmissionCommentsComponent,
],
imports: [
CoreSharedModule,
CoreCommentsComponentsModule,
],
providers: [
AddonModAssignSubmissionCommentsHandler,
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionCommentsHandler.instance);
},
},
],
exports: [
AddonModAssignSubmissionCommentsComponent,
],
entryComponents: [
AddonModAssignSubmissionCommentsComponent,
],
})
export class AddonModAssignSubmissionCommentsModule {}

View File

@ -0,0 +1,8 @@
<ion-item *ngIf="commentsEnabled" class="ion-text-wrap" (click)="showComments($event)" detail="false">
<ion-label>
<h2>{{plugin.name}}</h2>
<core-comments contextLevel="module" [instanceId]="assign.cmid" component="assignsubmission_comments"
[itemId]="submission.id" area="submission_comments" [title]="plugin.name" [courseId]="assign.course">
</core-comments>
</ion-label>
</ion-item>

View File

@ -0,0 +1,61 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild } from '@angular/core';
import { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin';
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
import { CoreComments } from '@features/comments/services/comments';
/**
* Component to render a comments submission plugin.
*/
@Component({
selector: 'addon-mod-assign-submission-comments',
templateUrl: 'addon-mod-assign-submission-comments.html',
})
export class AddonModAssignSubmissionCommentsComponent extends AddonModAssignSubmissionPluginComponent {
@ViewChild(CoreCommentsCommentsComponent) commentsComponent!: CoreCommentsCommentsComponent;
commentsEnabled: boolean;
constructor() {
super();
this.commentsEnabled = !CoreComments.instance.areCommentsDisabledInSite();
}
/**
* Invalidate the data.
*
* @return Promise resolved when done.
*/
invalidate(): Promise<void> {
return CoreComments.instance.invalidateCommentsData(
'module',
this.assign.cmid,
'assignsubmission_comments',
this.submission.id,
'submission_comments',
);
}
/**
* Show the comments.
*/
showComments(e?: Event): void {
this.commentsComponent?.openComments(e);
}
}

View File

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

View File

@ -0,0 +1,107 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from '@addons/mod/assign/services/assign';
import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreComments } from '@features/comments/services/comments';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { AddonModAssignSubmissionCommentsComponent } from '../component/comments';
/**
* Handler for comments submission plugin.
*/
@Injectable( { providedIn: 'root' })
export class AddonModAssignSubmissionCommentsHandlerService implements AddonModAssignSubmissionHandler {
name = 'AddonModAssignSubmissionCommentsHandler';
type = 'comments';
/**
* Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the
* plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit
* unfiltered data.
*
* @return Boolean or promise resolved with boolean: whether it can be edited in offline.
*/
canEditOffline(): boolean {
// This plugin is read only, but return true to prevent blocking the edition.
return true;
}
/**
* Return the Component to use to display the plugin data, either in read or in edit mode.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @param plugin The plugin object.
* @param edit Whether the user is editing.
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(plugin: AddonModAssignPlugin, edit = false): Type<unknown> | undefined {
return edit ? undefined : AddonModAssignSubmissionCommentsComponent;
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return True or promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Whether or not the handler is enabled for edit on a site level.
*
* @return Whether or not the handler is enabled for edit on a site level.
*/
isEnabledForEdit(): boolean{
return true;
}
/**
* Prefetch any required data for the plugin.
* This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async prefetch(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<void> {
// Fail silently (Moodle < 3.1.1, 3.2)
await CoreUtils.instance.ignoreErrors(
CoreComments.instance.getComments(
'module',
assign.cmid,
'assignsubmission_comments',
submission.id,
'submission_comments',
0,
siteId,
),
);
}
}
export const AddonModAssignSubmissionCommentsHandler = makeSingleton(AddonModAssignSubmissionCommentsHandlerService);

View File

@ -0,0 +1,19 @@
<!-- Read only. -->
<ion-item class="ion-text-wrap" *ngIf="files && files.length && !edit">
<ion-label>
<h2>{{ plugin.name }}</h2>
<div lines="none">
<core-files [files]="files" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-files>
</div>
</ion-label>
</ion-item>
<!-- Edit -->
<div *ngIf="edit">
<ion-item-divider class="ion-text-wrap" sticky="true">
<ion-label><h2>{{ plugin.name }}</h2></ion-label>
</ion-item-divider>
<core-attachments [files]="files" [maxSize]="maxSize" [maxSubmissions]="maxSubmissions"
[component]="component" [componentId]="assign.cmid" [acceptedTypes]="acceptedTypes" [allowOffline]="allowOffline">
</core-attachments>
</div>

View File

@ -0,0 +1,85 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin';
import { AddonModAssign, AddonModAssignProvider } from '@addons/mod/assign/services/assign';
import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper';
import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline';
import { Component, OnInit } from '@angular/core';
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CoreFileSession } from '@services/file-session';
import { CoreUtils } from '@services/utils/utils';
import { AddonModAssignSubmissionFileHandlerService } from '../services/handler';
import { FileEntry } from '@ionic-native/file/ngx';
/**
* Component to render a file submission plugin.
*/
@Component({
selector: 'addon-mod-assign-submission-file',
templateUrl: 'addon-mod-assign-submission-file.html',
})
export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmissionPluginComponent implements OnInit {
component = AddonModAssignProvider.COMPONENT;
maxSize?: number;
acceptedTypes?: string;
maxSubmissions?: number;
/**
* Component being initialized.
*/
async nOnInit(): Promise<void> {
// Get the offline data.
const filesData = await CoreUtils.instance.ignoreErrors(
AddonModAssignOffline.instance.getSubmission(this.assign.id),
undefined,
);
this.acceptedTypes = this.data?.configs.filetypeslist;
this.maxSize = this.data?.configs.maxsubmissionsizebytes
? parseInt(this.data?.configs.maxsubmissionsizebytes, 10)
: undefined;
this.maxSubmissions = this.data?.configs.maxfilesubmissions
? parseInt(this.data?.configs.maxfilesubmissions, 10)
: undefined;
try {
if (filesData && filesData.plugindata && filesData.plugindata.files_filemanager) {
const offlineDataFiles = <CoreFileUploaderStoreFilesResult>filesData.plugindata.files_filemanager;
// It has offline data.
let offlineFiles: FileEntry[] = [];
if (offlineDataFiles.offline) {
offlineFiles = <FileEntry[]>await CoreUtils.instance.ignoreErrors(
AddonModAssignHelper.instance.getStoredSubmissionFiles(
this.assign.id,
AddonModAssignSubmissionFileHandlerService.FOLDER_NAME,
),
[],
);
}
this.files = offlineDataFiles.online || [];
this.files = this.files.concat(offlineFiles);
} else {
// No offline data, get the online files.
this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin);
}
} finally {
CoreFileSession.instance.setFiles(this.component, this.assign.id, this.files);
}
}
}

View File

@ -0,0 +1,46 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonModAssignSubmissionFileHandler } from './services/handler';
import { AddonModAssignSubmissionFileComponent } from './component/file';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate';
@NgModule({
declarations: [
AddonModAssignSubmissionFileComponent,
],
imports: [
CoreSharedModule,
],
providers: [
AddonModAssignSubmissionFileHandler,
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionFileHandler.instance);
},
},
],
exports: [
AddonModAssignSubmissionFileComponent,
],
entryComponents: [
AddonModAssignSubmissionFileComponent,
],
})
export class AddonModAssignSubmissionFileModule {}

View File

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

View File

@ -0,0 +1,388 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
AddonModAssignAssign,
AddonModAssignSubmission,
AddonModAssignPlugin,
AddonModAssignProvider,
AddonModAssign,
} from '@addons/mod/assign/services/assign';
import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper';
import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } from '@addons/mod/assign/services/assign-offline';
import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CoreFileHelper } from '@services/file-helper';
import { CoreFileSession } from '@services/file-session';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import { AddonModAssignSubmissionFileComponent } from '../component/file';
import { FileEntry } from '@ionic-native/file/ngx';
/**
* Handler for file submission plugin.
*/
@Injectable( { providedIn: 'root' })
export class AddonModAssignSubmissionFileHandlerService implements AddonModAssignSubmissionHandler {
static readonly FOLDER_NAME = 'submission_file';
name = 'AddonModAssignSubmissionFileHandler';
type = 'file';
/**
* Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the
* plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit
* unfiltered data.
*
* @return Boolean or promise resolved with boolean: whether it can be edited in offline.
*/
canEditOffline(): boolean {
// This plugin doesn't use Moodle filters, it can be edited in offline.
return true;
}
/**
* Check if a plugin has no data.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @return Whether the plugin is empty.
*/
isEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean {
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
return files.length === 0;
}
/**
* Should clear temporary data for a cancelled submission.
*
* @param assign The assignment.
*/
clearTmpData(assign: AddonModAssignAssign): void {
const files = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
// Clear the files in session for this assign.
CoreFileSession.instance.clearFiles(AddonModAssignProvider.COMPONENT, assign.id);
// Now delete the local files from the tmp folder.
CoreFileUploader.instance.clearTmpFiles(files);
}
/**
* This function will be called when the user wants to create a new submission based on the previous one.
* It should add to pluginData the data to send to server based in the data in plugin (previous attempt).
*
* @param assign The assignment.
* @param plugin The plugin object.
* @param pluginData Object where to store the data to send.
* @return If the function is async, it should return a Promise resolved when done.
*/
async copySubmissionData(
assign: AddonModAssignAssign,
plugin: AddonModAssignPlugin,
pluginData: AddonModAssignSubmissionFilePluginData,
): Promise<void> {
// We need to re-upload all the existing files.
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
// Get the itemId.
pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadFiles(assign.id, files);
}
/**
* Return the Component to use to display the plugin data, either in read or in edit mode.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): Type<unknown> {
return AddonModAssignSubmissionFileComponent;
}
/**
* Delete any stored data for the plugin and submission.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param offlineData Offline data stored.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
async deleteOfflineData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
siteId?: string,
): Promise<void> {
await CoreUtils.instance.ignoreErrors(
AddonModAssignHelper.instance.deleteStoredSubmissionFiles(
assign.id,
AddonModAssignSubmissionFileHandlerService.FOLDER_NAME,
submission.userid,
siteId,
),
);
}
/**
* Get files used by this plugin.
* The files returned by this function will be prefetched when the user prefetches the assign.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return The files (or promise resolved with the files).
*/
getPluginFiles(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
): CoreWSExternalFile[] {
return AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
}
/**
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @return The size (or promise resolved with size).
*/
async getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise<number> {
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
return CoreFileHelper.instance.getTotalFilesSize(files);
}
/**
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @return The size (or promise resolved with size).
*/
async getSizeForEdit(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
): Promise<number> {
// Check if there's any change.
if (this.hasDataChanged(assign, submission, plugin)) {
const files = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
return CoreFileHelper.instance.getTotalFilesSize(files);
} else {
// Nothing has changed, we won't upload any file.
return 0;
}
}
/**
* Check if the submission data has changed for this plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @return Boolean (or promise resolved with boolean): whether the data has changed.
*/
async hasDataChanged(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
): Promise<boolean> {
const offlineData = await CoreUtils.instance.ignoreErrors(
// Check if there's any offline data.
AddonModAssignOffline.instance.getSubmission(assign.id, submission.userid),
undefined,
);
let numFiles: number;
if (offlineData && offlineData.plugindata && offlineData.plugindata.files_filemanager) {
const offlineDataFiles = <CoreFileUploaderStoreFilesResult>offlineData.plugindata.files_filemanager;
// Has offline data, return the number of files.
numFiles = offlineDataFiles.offline + offlineDataFiles.online.length;
} else {
// No offline data, return the number of online files.
const pluginFiles = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
numFiles = pluginFiles && pluginFiles.length;
}
const currentFiles = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
if (currentFiles.length != numFiles) {
// Number of files has changed.
return true;
}
const files = await this.getSubmissionFilesToSync(assign, submission, offlineData);
// Check if there is any local file added and list has changed.
return CoreFileUploader.instance.areFileListDifferent(currentFiles, files);
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return True or promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Whether or not the handler is enabled for edit on a site level.
*
* @return Whether or not the handler is enabled for edit on a site level.
*/
isEnabledForEdit(): boolean {
return true;
}
/**
* Prepare and add to pluginData the data to send to the server based on the input data.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @param pluginData Object where to store the data to send.
* @param offline Whether the user is editing in offline.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
async prepareSubmissionData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: AddonModAssignSubmissionFileData,
pluginData: AddonModAssignSubmissionFilePluginData,
offline?: boolean,
userId?: number,
siteId?: string,
): Promise<void> {
const changed = await this.hasDataChanged(assign, submission, plugin);
if (!changed) {
return;
}
// Data has changed, we need to upload new files and re-upload all the existing files.
const currentFiles = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
const error = CoreUtils.instance.hasRepeatedFilenames(currentFiles);
if (error) {
throw error;
}
pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadOrStoreFiles(
assign.id,
AddonModAssignSubmissionFileHandlerService.FOLDER_NAME,
currentFiles,
offline,
userId,
siteId,
);
}
/**
* Prepare and add to pluginData the data to send to the server based on the offline data stored.
* This will be used when performing a synchronization.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param offlineData Offline data stored.
* @param pluginData Object where to store the data to send.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
async prepareSyncData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
pluginData: AddonModAssignSubmissionFilePluginData,
siteId?: string,
): Promise<void> {
const files = await this.getSubmissionFilesToSync(assign, submission, offlineData, siteId);
if (files.length == 0) {
return;
}
pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadFiles(assign.id, files, siteId);
}
/**
* Get the file list to be synced.
*
* @param assign The assignment.
* @param submission The submission.
* @param offlineData Offline data stored.
* @param siteId Site ID. If not defined, current site.
* @return File entries when is all resolved.
*/
protected async getSubmissionFilesToSync(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
offlineData?: AddonModAssignSubmissionsDBRecordFormatted,
siteId?: string,
): Promise<(FileEntry | CoreWSExternalFile)[]> {
const filesData = <CoreFileUploaderStoreFilesResult>offlineData?.plugindata.files_filemanager;
if (!filesData) {
return [];
}
// Has some data to sync.
let files: (FileEntry | CoreWSExternalFile)[] = filesData.online || [];
if (filesData.offline) {
// Has offline files, get them and add them to the list.
const storedFiles = <FileEntry[]> await CoreUtils.instance.ignoreErrors(
AddonModAssignHelper.instance.getStoredSubmissionFiles(
assign.id,
AddonModAssignSubmissionFileHandlerService.FOLDER_NAME,
submission.userid,
siteId,
),
[],
);
files = files.concat(storedFiles);
}
return files;
}
}
export const AddonModAssignSubmissionFileHandler = makeSingleton(AddonModAssignSubmissionFileHandlerService);
// Define if ever used.
export type AddonModAssignSubmissionFileData = Record<string, unknown>;
export type AddonModAssignSubmissionFilePluginData = {
// The id of a draft area containing files for this submission. Or the offline file results.
files_filemanager: number | CoreFileUploaderStoreFilesResult; // eslint-disable-line @typescript-eslint/naming-convention
};

View File

@ -0,0 +1,35 @@
<!-- Read only -->
<ion-item class="ion-text-wrap" *ngIf="!edit && text">
<ion-label>
<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" contextLevel="module" [contextInstanceId]="assign.cmid"
[courseId]="assign.course">
</core-format-text>
</p>
</ion-label>
</ion-item>
<!-- Edit -->
<div *ngIf="edit && loaded">
<ion-item-divider class="ion-text-wrap" sticky="true">
<ion-label><h2>{{ plugin.name }}</h2></ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap" *ngIf="wordLimitEnabled && words >= 0">
<ion-label>
<h2>{{ 'addon.mod_assign.wordlimit' | translate }}</h2>
<p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + wordLimit} }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-rich-text-editor [control]="control" [placeholder]="plugin.name"
name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component"
[componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid"
elementId="onlinetext_editor" [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}">
</core-rich-text-editor>
</ion-label>
</ion-item>
</div>

View File

@ -0,0 +1,130 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin';
import { AddonModAssignProvider, AddonModAssign } from '@addons/mod/assign/services/assign';
import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline';
import { Component, OnInit, ElementRef } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { AddonModAssignSubmissionOnlineTextPluginData } from '../services/handler';
/**
* Component to render an onlinetext submission plugin.
*/
@Component({
selector: 'addon-mod-assign-submission-online-text',
templateUrl: 'addon-mod-assign-submission-onlinetext.html',
})
export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignSubmissionPluginComponent implements OnInit {
control?: FormControl;
words = 0;
component = AddonModAssignProvider.COMPONENT;
text = '';
loaded = false;
wordLimitEnabled = false;
currentUserId: number;
wordLimit = 0;
protected wordCountTimeout?: number;
protected element: HTMLElement;
constructor(
protected fb: FormBuilder,
element: ElementRef,
) {
super();
this.element = element.nativeElement;
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
}
/**
* Component being initialized.
*/
async nOnInit(): Promise<void> {
// Get the text. Check if we have anything offline.
const offlineData = await CoreUtils.instance.ignoreErrors(
AddonModAssignOffline.instance.getSubmission(this.assign.id),
undefined,
);
this.wordLimitEnabled = !!parseInt(this.data?.configs.wordlimitenabled || '0', 10);
this.wordLimit = parseInt(this.data?.configs.wordlimit || '0');
try {
if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) {
this.text = (<AddonModAssignSubmissionOnlineTextPluginData>offlineData.plugindata).onlinetext_editor.text;
} else {
// No offline data found, return online text.
this.text = AddonModAssign.instance.getSubmissionPluginText(this.plugin);
}
// Set the text.
if (!this.edit) {
// Not editing, see full text when clicked.
this.element.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (this.text) {
// Open a new state with the interpolated contents.
CoreTextUtils.instance.viewText(this.plugin.name, this.text, {
component: this.component,
componentId: this.assign.cmid,
filter: true,
contextLevel: 'module',
instanceId: this.assign.cmid,
courseId: this.assign.course,
});
}
});
} else {
// Create and add the control.
this.control = this.fb.control(this.text);
}
// Calculate initial words.
if (this.wordLimitEnabled) {
this.words = CoreTextUtils.instance.countWords(this.text);
}
} finally {
this.loaded = true;
}
}
/**
* Text changed.
*
* @param text The new text.
*/
onChange(text: string): void {
// Count words if needed.
if (this.wordLimitEnabled) {
// Cancel previous wait.
clearTimeout(this.wordCountTimeout);
// Wait before calculating, if the user keeps inputing we won't calculate.
// This is to prevent slowing down devices, this calculation can be slow if the text is long.
this.wordCountTimeout = window.setTimeout(() => {
this.words = CoreTextUtils.instance.countWords(text);
}, 1500);
}
}
}

View File

@ -0,0 +1,4 @@
{
"pluginname": "Online text submissions",
"wordlimitexceeded": "The word limit for this assignment is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please review your submission and try again."
}

View File

@ -0,0 +1,48 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonModAssignSubmissionOnlineTextHandler } from './services/handler';
import { AddonModAssignSubmissionOnlineTextComponent } from './component/onlinetext';
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate';
@NgModule({
declarations: [
AddonModAssignSubmissionOnlineTextComponent,
],
imports: [
CoreSharedModule,
CoreEditorComponentsModule,
],
providers: [
AddonModAssignSubmissionOnlineTextHandler,
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionOnlineTextHandler.instance);
},
},
],
exports: [
AddonModAssignSubmissionOnlineTextComponent,
],
entryComponents: [
AddonModAssignSubmissionOnlineTextComponent,
],
})
export class AddonModAssignSubmissionOnlineTextModule {}

View File

@ -0,0 +1,323 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
AddonModAssignAssign,
AddonModAssignSubmission,
AddonModAssignPlugin,
AddonModAssign,
} from '@addons/mod/assign/services/assign';
import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper';
import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } from '@addons/mod/assign/services/assign-offline';
import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreFileHelper } from '@services/file-helper';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
import { AddonModAssignSubmissionOnlineTextComponent } from '../component/onlinetext';
/**
* Handler for online text submission plugin.
*/
@Injectable( { providedIn: 'root' })
export class AddonModAssignSubmissionOnlineTextHandlerService implements AddonModAssignSubmissionHandler {
name = 'AddonModAssignSubmissionOnlineTextHandler';
type = 'onlinetext';
/**
* Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the
* plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit
* unfiltered data.
*
* @return Boolean or promise resolved with boolean: whether it can be edited in offline.
*/
canEditOffline(): boolean {
// This plugin uses Moodle filters, it cannot be edited in offline.
return false;
}
/**
* Check if a plugin has no data.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @return Whether the plugin is empty.
*/
isEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean {
const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true);
// If the text is empty, we can ignore files because they won't be visible anyways.
return text.trim().length === 0;
}
/**
* This function will be called when the user wants to create a new submission based on the previous one.
* It should add to pluginData the data to send to server based in the data in plugin (previous attempt).
*
* @param assign The assignment.
* @param plugin The plugin object.
* @param pluginData Object where to store the data to send.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
async copySubmissionData(
assign: AddonModAssignAssign,
plugin: AddonModAssignPlugin,
pluginData: AddonModAssignSubmissionOnlineTextPluginData,
userId?: number,
siteId?: string,
): Promise<void> {
const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true);
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
let itemId = 0;
if (files.length) {
// Re-upload the files.
itemId = await AddonModAssignHelper.instance.uploadFiles(assign.id, files, siteId);
}
pluginData.onlinetext_editor = {
text: text,
format: 1,
itemid: itemId,
};
}
/**
* Return the Component to use to display the plugin data, either in read or in edit mode.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): Type<unknown> {
return AddonModAssignSubmissionOnlineTextComponent;
}
/**
* Get files used by this plugin.
* The files returned by this function will be prefetched when the user prefetches the assign.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @return The files (or promise resolved with the files).
*/
getPluginFiles(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
): CoreWSExternalFile[] {
return AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
}
/**
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @return The size (or promise resolved with size).
*/
async getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise<number> {
const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true);
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
const filesSize = await CoreFileHelper.instance.getTotalFilesSize(files);
return text.length + filesSize;
}
/**
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @return The size (or promise resolved with size).
*/
getSizeForEdit(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
): number {
const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true);
return text.length;
}
/**
* Get the text to submit.
*
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @return Text to submit.
*/
protected getTextToSubmit(plugin: AddonModAssignPlugin, inputData: AddonModAssignSubmissionOnlineTextData): string {
const text = inputData.onlinetext_editor_text;
const files = plugin.fileareas && plugin.fileareas[0] && plugin.fileareas[0].files || [];
return CoreTextUtils.instance.restorePluginfileUrls(text, files || []);
}
/**
* Check if the submission data has changed for this plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @return Boolean (or promise resolved with boolean): whether the data has changed.
*/
async hasDataChanged(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: AddonModAssignSubmissionOnlineTextData,
): Promise<boolean> {
// Get the original text from plugin or offline.
const offlineData =
await CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getSubmission(assign.id, submission.userid));
let initialText = '';
if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) {
initialText = (<AddonModAssignSubmissionOnlineTextPluginData>offlineData.plugindata).onlinetext_editor.text;
} else {
// No offline data found, get text from plugin.
initialText = plugin.editorfields && plugin.editorfields[0] ? plugin.editorfields[0].text : '';
}
// Check if text has changed.
return initialText != this.getTextToSubmit(plugin, inputData);
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return True or promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Whether or not the handler is enabled for edit on a site level.
*
* @return Whether or not the handler is enabled for edit on a site level.
*/
isEnabledForEdit(): boolean {
// There's a bug in Moodle 3.1.0 that doesn't allow submitting HTML, so we'll disable this plugin in that case.
// Bug was fixed in 3.1.1 minor release and in 3.2.
const currentSite = CoreSites.instance.getCurrentSite();
return !!currentSite?.isVersionGreaterEqualThan('3.1.1') || !!currentSite?.checkIfAppUsesLocalMobile();
}
/**
* Prepare and add to pluginData the data to send to the server based on the input data.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @param pluginData Object where to store the data to send.
* @param offline Whether the user is editing in offline.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
prepareSubmissionData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: AddonModAssignSubmissionOnlineTextData,
pluginData: AddonModAssignSubmissionOnlineTextPluginData,
): void | Promise<void> {
let text = this.getTextToSubmit(plugin, inputData);
// Check word limit.
const configs = AddonModAssignHelper.instance.getPluginConfig(assign, 'assignsubmission', plugin.type);
if (parseInt(configs.wordlimitenabled, 10)) {
const words = CoreTextUtils.instance.countWords(text);
const wordlimit = parseInt(configs.wordlimit, 10);
if (words > wordlimit) {
const params = { $a: { count: words, limit: wordlimit } };
const message = Translate.instance.instant('addon.mod_assign_submission_onlinetext.wordlimitexceeded', params);
throw new CoreError(message);
}
}
// Add some HTML to the text if needed.
text = CoreTextUtils.instance.formatHtmlLines(text);
pluginData.onlinetext_editor = {
text: text,
format: 1,
itemid: 0, // Can't add new files yet, so we use a fake itemid.
};
}
/**
* Prepare and add to pluginData the data to send to the server based on the offline data stored.
* This will be used when performing a synchronization.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param offlineData Offline data stored.
* @param pluginData Object where to store the data to send.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
prepareSyncData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
pluginData: AddonModAssignSubmissionOnlineTextPluginData,
): void | Promise<void> {
const offlinePluginData = <AddonModAssignSubmissionOnlineTextPluginData>(offlineData && offlineData.plugindata);
const textData = offlinePluginData.onlinetext_editor;
if (textData) {
// Has some data to sync.
pluginData.onlinetext_editor = textData;
}
}
}
export const AddonModAssignSubmissionOnlineTextHandler = makeSingleton(AddonModAssignSubmissionOnlineTextHandlerService);
export type AddonModAssignSubmissionOnlineTextData = {
// The text for this submission.
onlinetext_editor_text: string; // eslint-disable-line @typescript-eslint/naming-convention
};
export type AddonModAssignSubmissionOnlineTextPluginData = {
// Editor structure.
onlinetext_editor: { // eslint-disable-line @typescript-eslint/naming-convention
text: string; // The text for this submission.
format: number; // The format for this submission.
itemid: number; // The draft area id for files attached to the submission.
};
};

View File

@ -0,0 +1,27 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { AddonModAssignSubmissionCommentsModule } from './comments/comments.module';
import { AddonModAssignSubmissionFileModule } from './file/file.module';
import { AddonModAssignSubmissionOnlineTextModule } from './onlinetext/onlinetext.module';
@NgModule({
imports: [
AddonModAssignSubmissionCommentsModule,
AddonModAssignSubmissionFileModule,
AddonModAssignSubmissionOnlineTextModule,
],
})
export class AddonModAssignSubmissionModule { }