MOBILE-3636 assign: Submission plugins
parent
3281196ec0
commit
85f79bb944
|
@ -29,6 +29,7 @@ import { AddonModAssignModuleHandler, AddonModAssignModuleHandlerService } from
|
|||
import { AddonModAssignPrefetchHandler } from './services/handlers/prefetch';
|
||||
import { AddonModAssignPushClickHandler } from './services/handlers/push-click';
|
||||
import { AddonModAssignSyncCronHandler } from './services/handlers/sync-cron';
|
||||
import { AddonModAssignSubmissionModule } from './submission/submission.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -41,6 +42,7 @@ const routes: Routes = [
|
|||
imports: [
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
AddonModAssignComponentsModule,
|
||||
AddonModAssignSubmissionModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from '../../services/assign';
|
||||
import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper';
|
||||
import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate';
|
||||
import { FileEntry } from '@ionic-native/file/ngx';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
text = '';
|
||||
files: CoreWSExternalFile[] = [];
|
||||
files: (FileEntry | CoreWSExternalFile)[] = [];
|
||||
notSupported = false;
|
||||
pluginLoaded = false;
|
||||
|
||||
|
|
|
@ -392,7 +392,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
try {
|
||||
return AddonModAssignHelper.instance.hasFeedbackDataChanged(
|
||||
this.assign!,
|
||||
this.userSubmission,
|
||||
this.userSubmission!, // @todo
|
||||
this.feedback,
|
||||
this.submitId,
|
||||
);
|
||||
|
|
|
@ -492,7 +492,7 @@ export class AddonModAssignHelperProvider {
|
|||
*/
|
||||
async hasFeedbackDataChanged(
|
||||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted,
|
||||
feedback: AddonModAssignSubmissionFeedback,
|
||||
userId: number,
|
||||
): Promise<boolean> {
|
||||
|
@ -683,15 +683,13 @@ export class AddonModAssignHelperProvider {
|
|||
offline = false,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
): Promise<number | CoreFileUploaderStoreFilesResult> {
|
||||
|
||||
if (offline) {
|
||||
await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
|
||||
|
||||
return;
|
||||
return await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
|
||||
}
|
||||
|
||||
await this.uploadFiles(assignId, files, siteId);
|
||||
return await this.uploadFiles(assignId, files, siteId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { CoreApp } from '@services/app';
|
|||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { AddonModAssignOffline } from './assign-offline';
|
||||
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
|
||||
import { CoreComments } from '@features/comments/services/comments';
|
||||
|
||||
const ROOT_CACHE_KEY = 'mmaModAssign:';
|
||||
|
||||
|
@ -754,7 +755,7 @@ export class AddonModAssignProvider {
|
|||
promises.push(this.invalidateAssignmentUserMappingsData(assign.id, siteId));
|
||||
promises.push(this.invalidateAssignmentGradesData(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(CoreGrades.instance.invalidateAllCourseGradesData(courseId));
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { AddonModAssignDefaultFeedbackHandler } from './handlers/default-feedbac
|
|||
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { AddonModAssignSubmissionFormatted } from './assign-helper';
|
||||
|
||||
/**
|
||||
* Interface that all feedback handlers must implement.
|
||||
|
@ -264,7 +265,7 @@ export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonMod
|
|||
*/
|
||||
async hasPluginDataChanged(
|
||||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
userId: number,
|
||||
|
|
|
@ -18,6 +18,7 @@ import { AddonModAssignDefaultSubmissionHandler } from './handlers/default-submi
|
|||
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { AddonModAssignSubmissionsDBRecordFormatted } from './assign-offline';
|
||||
|
||||
/**
|
||||
* Interface that all submission handlers must implement.
|
||||
|
@ -69,7 +70,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
inputData: Record<string, unknown>,
|
||||
): void;
|
||||
|
||||
/**
|
||||
|
@ -105,9 +106,9 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
offlineData: any,
|
||||
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
|
||||
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.
|
||||
|
@ -172,7 +173,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
inputData: Record<string, unknown>,
|
||||
): number | Promise<number>;
|
||||
|
||||
/**
|
||||
|
@ -188,7 +189,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
inputData: Record<string, unknown>,
|
||||
): boolean | Promise<boolean>;
|
||||
|
||||
/**
|
||||
|
@ -232,12 +233,12 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
inputData: Record<string, unknown>,
|
||||
pluginData: AddonModAssignSavePluginData,
|
||||
offline?: boolean,
|
||||
userId?: number,
|
||||
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.
|
||||
|
@ -255,10 +256,10 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
offlineData: any,
|
||||
pluginData: any,
|
||||
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
|
||||
pluginData: AddonModAssignSavePluginData,
|
||||
siteId?: string,
|
||||
): void | Promise<any>;
|
||||
): void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -303,7 +304,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
inputData: Record<string, unknown>,
|
||||
): void {
|
||||
return this.executeFunctionOnEnabled(plugin.type, 'clearTmpData', [assign, submission, plugin, inputData]);
|
||||
}
|
||||
|
@ -346,9 +347,9 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
offlineData: any,
|
||||
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
|
||||
siteId?: string,
|
||||
): Promise<any | undefined> {
|
||||
): Promise<void> {
|
||||
return await this.executeFunctionOnEnabled(
|
||||
plugin.type,
|
||||
'deleteOfflineData',
|
||||
|
@ -423,7 +424,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
inputData: Record<string, unknown>,
|
||||
): Promise<number | undefined> {
|
||||
return await this.executeFunctionOnEnabled(
|
||||
plugin.type,
|
||||
|
@ -445,7 +446,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
inputData: Record<string, unknown>,
|
||||
): Promise<boolean | undefined> {
|
||||
return await this.executeFunctionOnEnabled(
|
||||
plugin.type,
|
||||
|
@ -520,12 +521,12 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
pluginData: any,
|
||||
inputData: Record<string, unknown>,
|
||||
pluginData: AddonModAssignSavePluginData,
|
||||
offline?: boolean,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<any | undefined> {
|
||||
): Promise<void | undefined> {
|
||||
|
||||
return await this.executeFunctionOnEnabled(
|
||||
plugin.type,
|
||||
|
@ -549,10 +550,10 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
|
|||
assign: AddonModAssignAssign,
|
||||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
offlineData: any,
|
||||
pluginData: any,
|
||||
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
|
||||
pluginData: AddonModAssignSavePluginData,
|
||||
siteId?: string,
|
||||
): Promise<any | undefined> {
|
||||
): Promise<void> {
|
||||
|
||||
return this.executeFunctionOnEnabled(
|
||||
plugin.type,
|
||||
|
@ -562,4 +563,4 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
|
|||
}
|
||||
|
||||
}
|
||||
export class AddonModAssignSubmissionDelegate extends makeSingleton(AddonModAssignSubmissionDelegateService) {}
|
||||
export const AddonModAssignSubmissionDelegate = makeSingleton(AddonModAssignSubmissionDelegateService);
|
||||
|
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pluginname": "Submission comments"
|
||||
}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pluginname": "File submissions"
|
||||
}
|
|
@ -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
|
||||
};
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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."
|
||||
}
|
|
@ -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 {}
|
|
@ -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.
|
||||
};
|
||||
};
|
|
@ -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 { }
|
Loading…
Reference in New Issue