forked from CIT/Vmeda.Online
		
	
						commit
						fc39c3e30e
					
				@ -51,8 +51,7 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        await this.fetchInitialBadges();
 | 
			
		||||
 | 
			
		||||
        this.badges.watchSplitViewOutlet(this.splitView);
 | 
			
		||||
        this.badges.start();
 | 
			
		||||
        this.badges.start(this.splitView);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -19,11 +19,13 @@ import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
 | 
			
		||||
 | 
			
		||||
import { AddonCalendarEditEventPage } from './edit-event.page';
 | 
			
		||||
import { CanLeaveGuard } from '@guards/can-leave';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonCalendarEditEventPage,
 | 
			
		||||
        canDeactivate: [CanLeaveGuard],
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
 | 
			
		||||
import { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
@ -27,9 +27,9 @@ import {
 | 
			
		||||
import { AddonCalendarOffline } from './calendar-offline';
 | 
			
		||||
import { AddonCalendarHelper } from './calendar-helper';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to sync calendar.
 | 
			
		||||
@ -52,21 +52,23 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    async syncAllEvents(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
    async syncAllEvents(siteId?: string, force = false): Promise<void> {
 | 
			
		||||
        await this.syncOnSites('all calendar events', this.syncAllEventsFunc.bind(this, [force]), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync all events on a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID to sync.
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @param siteId Site ID to sync.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncAllEventsFunc(siteId: string, force?: boolean): Promise<void> {
 | 
			
		||||
        const result = await (force ? this.syncEvents(siteId) : this.syncEventsIfNeeded(siteId));
 | 
			
		||||
    protected async syncAllEventsFunc(force = false, siteId?: string): Promise<void> {
 | 
			
		||||
        const result = force
 | 
			
		||||
            ? await this.syncEvents(siteId)
 | 
			
		||||
            : await this.syncEventsIfNeeded(siteId);
 | 
			
		||||
 | 
			
		||||
        if (result && result.updated) {
 | 
			
		||||
        if (result?.updated) {
 | 
			
		||||
            // Sync successful, send event.
 | 
			
		||||
            CoreEvents.trigger<AddonCalendarSyncEvents>(AddonCalendarSyncProvider.AUTO_SYNCED, result, siteId);
 | 
			
		||||
        }
 | 
			
		||||
@ -78,13 +80,13 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the events are synced or if it doesn't need to be synced.
 | 
			
		||||
     */
 | 
			
		||||
    async syncEventsIfNeeded(siteId?: string): Promise<void> {
 | 
			
		||||
    async syncEventsIfNeeded(siteId?: string): Promise<AddonCalendarSyncEvents | undefined> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const needed = await this.isSyncNeeded(AddonCalendarSyncProvider.SYNC_ID, siteId);
 | 
			
		||||
 | 
			
		||||
        if (needed) {
 | 
			
		||||
            await this.syncEvents(siteId);
 | 
			
		||||
            return this.syncEvents(siteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -125,17 +127,12 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
 | 
			
		||||
            updated: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let eventIds: number[] = [];
 | 
			
		||||
        try {
 | 
			
		||||
            eventIds = await AddonCalendarOffline.instance.getAllEventsIds(siteId);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // No offline data found.
 | 
			
		||||
        }
 | 
			
		||||
        const eventIds: number[] = await CoreUtils.instance.ignoreErrors(AddonCalendarOffline.instance.getAllEventsIds(siteId), []);
 | 
			
		||||
 | 
			
		||||
        if (eventIds.length > 0) {
 | 
			
		||||
            if (!CoreApp.instance.isOnline()) {
 | 
			
		||||
                // Cannot sync in offline.
 | 
			
		||||
                throw new CoreError('Cannot sync while offline');
 | 
			
		||||
                throw new CoreNetworkError();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const promises = eventIds.map((eventId) => this.syncOfflineEvent(eventId, result, siteId));
 | 
			
		||||
@ -175,10 +172,10 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
 | 
			
		||||
        if (CoreSync.instance.isBlocked(AddonCalendarProvider.COMPONENT, eventId, siteId)) {
 | 
			
		||||
            this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.');
 | 
			
		||||
 | 
			
		||||
            throw Translate.instance.instant(
 | 
			
		||||
            throw new CoreSyncBlockedError(Translate.instance.instant(
 | 
			
		||||
                'core.errorsyncblocked',
 | 
			
		||||
                { $a: Translate.instance.instant('addon.calendar.calendarevent') },
 | 
			
		||||
            );
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // First of all, check if the event has been deleted.
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ import { CoreUserDelegate } from '@features/user/services/user-delegate';
 | 
			
		||||
import { AddonMessagesSendMessageUserHandler } from './services/handlers/user-send-message';
 | 
			
		||||
import { Network, NgZone } from '@singletons';
 | 
			
		||||
import { AddonMessagesSync } from './services/messages-sync';
 | 
			
		||||
import { AddonMessagesSyncCronHandler } from './services/handlers/sync-cron';
 | 
			
		||||
 | 
			
		||||
const mainMenuChildrenRoutes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
@ -61,7 +62,7 @@ const mainMenuChildrenRoutes: Routes = [
 | 
			
		||||
                // Register handlers.
 | 
			
		||||
                CoreMainMenuDelegate.instance.registerHandler(AddonMessagesMainMenuHandler.instance);
 | 
			
		||||
                CoreCronDelegate.instance.register(AddonMessagesMainMenuHandler.instance);
 | 
			
		||||
                CoreCronDelegate.instance.register(AddonMessagesPushClickHandler.instance);
 | 
			
		||||
                CoreCronDelegate.instance.register(AddonMessagesSyncCronHandler.instance);
 | 
			
		||||
                CoreSettingsDelegate.instance.registerHandler(AddonMessagesSettingsHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.instance.registerHandler(AddonMessagesIndexLinkHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.instance.registerHandler(AddonMessagesDiscussionLinkHandler.instance);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										85
									
								
								src/addons/mod/assign/assign-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/addons/mod/assign/assign-lazy.module.ts
									
									
									
									
									
										Normal 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 { conditionalRoutes } from '@/app/app-routing.module';
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { CanLeaveGuard } from '@guards/can-leave';
 | 
			
		||||
import { CoreScreen } from '@services/screen';
 | 
			
		||||
import { AddonModAssignComponentsModule } from './components/components.module';
 | 
			
		||||
import { AddonModAssignEditPage } from './pages/edit/edit';
 | 
			
		||||
import { AddonModAssignIndexPage } from './pages/index/index.page';
 | 
			
		||||
import { AddonModAssignSubmissionListPage } from './pages/submission-list/submission-list.page';
 | 
			
		||||
import { AddonModAssignSubmissionReviewPage } from './pages/submission-review/submission-review';
 | 
			
		||||
 | 
			
		||||
const commonRoutes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId',
 | 
			
		||||
        component: AddonModAssignIndexPage,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/edit',
 | 
			
		||||
        component: AddonModAssignEditPage,
 | 
			
		||||
        canDeactivate: [CanLeaveGuard],
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const mobileRoutes: Routes = [
 | 
			
		||||
    ...commonRoutes,
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/submission',
 | 
			
		||||
        component: AddonModAssignSubmissionListPage,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/submission/:submitId',
 | 
			
		||||
        component: AddonModAssignSubmissionReviewPage,
 | 
			
		||||
        canDeactivate: [CanLeaveGuard],
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const tabletRoutes: Routes = [
 | 
			
		||||
    ...commonRoutes,
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/submission',
 | 
			
		||||
        component: AddonModAssignSubmissionListPage,
 | 
			
		||||
        children: [
 | 
			
		||||
            {
 | 
			
		||||
                path: ':submitId',
 | 
			
		||||
                component: AddonModAssignSubmissionReviewPage,
 | 
			
		||||
                canDeactivate: [CanLeaveGuard],
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    ...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile),
 | 
			
		||||
    ...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
        AddonModAssignComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModAssignIndexPage,
 | 
			
		||||
        AddonModAssignSubmissionListPage,
 | 
			
		||||
        AddonModAssignSubmissionReviewPage,
 | 
			
		||||
        AddonModAssignEditPage,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignLazyModule {}
 | 
			
		||||
							
								
								
									
										70
									
								
								src/addons/mod/assign/assign.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/addons/mod/assign/assign.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
			
		||||
// (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 { Routes } from '@angular/router';
 | 
			
		||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
 | 
			
		||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
			
		||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
 | 
			
		||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
			
		||||
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
 | 
			
		||||
import { CoreCronDelegate } from '@services/cron';
 | 
			
		||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
 | 
			
		||||
import { AddonModAssignComponentsModule } from './components/components.module';
 | 
			
		||||
import { AddonModAssignFeedbackModule } from './feedback/feedback.module';
 | 
			
		||||
import { OFFLINE_SITE_SCHEMA } from './services/database/assign';
 | 
			
		||||
import { AddonModAssignIndexLinkHandler } from './services/handlers/index-link';
 | 
			
		||||
import { AddonModAssignListLinkHandler } from './services/handlers/list-link';
 | 
			
		||||
import { AddonModAssignModuleHandler, AddonModAssignModuleHandlerService } from './services/handlers/module';
 | 
			
		||||
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 = [
 | 
			
		||||
    {
 | 
			
		||||
        path: AddonModAssignModuleHandlerService.PAGE_NAME,
 | 
			
		||||
        loadChildren: () => import('./assign-lazy.module').then(m => m.AddonModAssignLazyModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreMainMenuTabRoutingModule.forChild(routes),
 | 
			
		||||
        AddonModAssignComponentsModule,
 | 
			
		||||
        AddonModAssignSubmissionModule,
 | 
			
		||||
        AddonModAssignFeedbackModule,
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: CORE_SITE_SCHEMAS,
 | 
			
		||||
            useValue: [OFFLINE_SITE_SCHEMA],
 | 
			
		||||
            multi: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => () => {
 | 
			
		||||
                CoreCourseModuleDelegate.instance.registerHandler(AddonModAssignModuleHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.instance.registerHandler(AddonModAssignIndexLinkHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.instance.registerHandler(AddonModAssignListLinkHandler.instance);
 | 
			
		||||
                CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModAssignPrefetchHandler.instance);
 | 
			
		||||
                CoreCronDelegate.instance.register(AddonModAssignSyncCronHandler.instance);
 | 
			
		||||
                CorePushNotificationsDelegate.instance.registerClickHandler(AddonModAssignPushClickHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignModule {}
 | 
			
		||||
							
								
								
									
										45
									
								
								src/addons/mod/assign/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addons/mod/assign/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
// (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 { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
 | 
			
		||||
import { AddonModAssignIndexComponent } from './index/index';
 | 
			
		||||
import { AddonModAssignSubmissionComponent } from './submission/submission';
 | 
			
		||||
import { AddonModAssignSubmissionPluginComponent } from './submission-plugin/submission-plugin';
 | 
			
		||||
import { AddonModAssignFeedbackPluginComponent } from './feedback-plugin/feedback-plugin';
 | 
			
		||||
import { AddonModAssignEditFeedbackModalComponent } from './edit-feedback-modal/edit-feedback-modal';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModAssignIndexComponent,
 | 
			
		||||
        AddonModAssignSubmissionComponent,
 | 
			
		||||
        AddonModAssignSubmissionPluginComponent,
 | 
			
		||||
        AddonModAssignFeedbackPluginComponent,
 | 
			
		||||
        AddonModAssignEditFeedbackModalComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
        CoreCourseComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModAssignIndexComponent,
 | 
			
		||||
        AddonModAssignSubmissionComponent,
 | 
			
		||||
        AddonModAssignSubmissionPluginComponent,
 | 
			
		||||
        AddonModAssignFeedbackPluginComponent,
 | 
			
		||||
        AddonModAssignEditFeedbackModalComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignComponentsModule {}
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>{{ plugin.name }}</ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
 | 
			
		||||
                <ion-icon slot="icon-only" name="fas-times"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <form name="addon-mod_assign-edit-feedback-form" *ngIf="userId && plugin" #editFeedbackForm>
 | 
			
		||||
        <addon-mod-assign-feedback-plugin [assign]="assign" [submission]="submission" [userId]="userId"
 | 
			
		||||
            [plugin]="plugin" [edit]="true">
 | 
			
		||||
        </addon-mod-assign-feedback-plugin>
 | 
			
		||||
        <ion-button expand="block" (click)="done($event)">{{ 'core.done' | translate }}</ion-button>
 | 
			
		||||
    </form>
 | 
			
		||||
</ion-content>
 | 
			
		||||
@ -0,0 +1,100 @@
 | 
			
		||||
// (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, Input, ViewChild, ElementRef } from '@angular/core';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { ModalController, Translate } from '@singletons';
 | 
			
		||||
import { AddonModAssignAssign, AddonModAssignPlugin, AddonModAssignSubmission } from '../../services/assign';
 | 
			
		||||
import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Modal that allows editing a feedback plugin.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-assign-edit-feedback-modal',
 | 
			
		||||
    templateUrl: 'edit-feedback-modal.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignEditFeedbackModalComponent {
 | 
			
		||||
 | 
			
		||||
    @Input() assign!: AddonModAssignAssign; // The assignment.
 | 
			
		||||
    @Input() submission!: AddonModAssignSubmission; // The submission.
 | 
			
		||||
    @Input() plugin!: AddonModAssignPlugin; // The plugin object.
 | 
			
		||||
    @Input() userId!: number; // The user ID of the submission.
 | 
			
		||||
 | 
			
		||||
    @ViewChild('editFeedbackForm') formElement?: ElementRef;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close modal checking if there are changes first.
 | 
			
		||||
     *
 | 
			
		||||
     * @param data Data to return to the page.
 | 
			
		||||
     */
 | 
			
		||||
    async closeModal(): Promise<void> {
 | 
			
		||||
        const changed = await this.hasDataChanged();
 | 
			
		||||
        if (changed) {
 | 
			
		||||
            await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
        ModalController.instance.dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Done editing.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Click event.
 | 
			
		||||
     */
 | 
			
		||||
    done(e: Event): void {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, false, CoreSites.instance.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
        // Close the modal, sending the input data.
 | 
			
		||||
        ModalController.instance.dismiss(this.getInputData());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the input data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Object with the data.
 | 
			
		||||
     */
 | 
			
		||||
    protected getInputData(): Record<string, unknown> {
 | 
			
		||||
        return CoreDomUtils.instance.getDataFromForm(document.forms['addon-mod_assign-edit-feedback-form']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if data has changed.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved with boolean: whether the data has changed.
 | 
			
		||||
     */
 | 
			
		||||
    protected async hasDataChanged(): Promise<boolean> {
 | 
			
		||||
        const changed = await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
            AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(
 | 
			
		||||
                this.assign,
 | 
			
		||||
                this.submission,
 | 
			
		||||
                this.plugin,
 | 
			
		||||
                this.getInputData(),
 | 
			
		||||
                this.userId,
 | 
			
		||||
            ),
 | 
			
		||||
            true,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return !!changed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,23 @@
 | 
			
		||||
 | 
			
		||||
<core-dynamic-component [component]="pluginComponent" [data]="data">
 | 
			
		||||
    <!-- This content will be replaced by the component if found. -->
 | 
			
		||||
    <core-loading [hideUntil]="pluginLoaded">
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="text.length > 0 || files.length > 0">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <h2>{{ plugin.name }}</h2>
 | 
			
		||||
                <ion-badge *ngIf="notSupported" color="primary">
 | 
			
		||||
                    {{ 'addon.mod_assign.feedbacknotsupported' | translate }}
 | 
			
		||||
                </ion-badge>
 | 
			
		||||
                <p *ngIf="text">
 | 
			
		||||
                    <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>
 | 
			
		||||
                <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid"
 | 
			
		||||
                    [alwaysDownload]="true">
 | 
			
		||||
                </core-file>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</core-dynamic-component>
 | 
			
		||||
@ -0,0 +1,153 @@
 | 
			
		||||
// (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, Input, OnInit, ViewChild, Type } from '@angular/core';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { ModalController } from '@singletons';
 | 
			
		||||
import { AddonModAssignFeedbackCommentsTextData } from '../../feedback/comments/services/handler';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssignPlugin,
 | 
			
		||||
    AddonModAssignProvider,
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
} from '../../services/assign';
 | 
			
		||||
import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper';
 | 
			
		||||
import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate';
 | 
			
		||||
import { AddonModAssignEditFeedbackModalComponent } from '../edit-feedback-modal/edit-feedback-modal';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays an assignment feedback plugin.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-assign-feedback-plugin',
 | 
			
		||||
    templateUrl: 'addon-mod-assign-feedback-plugin.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignFeedbackPluginComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(CoreDynamicComponent) dynamicComponent!: CoreDynamicComponent;
 | 
			
		||||
 | 
			
		||||
    @Input() assign!: AddonModAssignAssign; // The assignment.
 | 
			
		||||
    @Input() submission!: AddonModAssignSubmission; // The submission.
 | 
			
		||||
    @Input() plugin!: AddonModAssignPlugin; // The plugin object.
 | 
			
		||||
    @Input() userId!: number; // The user ID of the submission.
 | 
			
		||||
    @Input() canEdit = false; // Whether the user can edit.
 | 
			
		||||
    @Input() edit = false; // Whether the user is editing.
 | 
			
		||||
 | 
			
		||||
    pluginComponent?: Type<unknown>; // Component to render the plugin.
 | 
			
		||||
    data?: AddonModAssignFeedbackPluginData; // Data to pass to the component.
 | 
			
		||||
 | 
			
		||||
    // Data to render the plugin if it isn't supported.
 | 
			
		||||
    component = AddonModAssignProvider.COMPONENT;
 | 
			
		||||
    text = '';
 | 
			
		||||
    files: CoreWSExternalFile[] = [];
 | 
			
		||||
    notSupported = false;
 | 
			
		||||
    pluginLoaded = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        if (!this.plugin) {
 | 
			
		||||
            this.pluginLoaded = true;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const name = AddonModAssignFeedbackDelegate.instance.getPluginName(this.plugin);
 | 
			
		||||
 | 
			
		||||
        if (!name) {
 | 
			
		||||
            this.pluginLoaded = true;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this.plugin.name = name;
 | 
			
		||||
 | 
			
		||||
        // Check if the plugin has defined its own component to render itself.
 | 
			
		||||
        this.pluginComponent = await AddonModAssignFeedbackDelegate.instance.getComponentForPlugin(this.plugin);
 | 
			
		||||
 | 
			
		||||
        if (this.pluginComponent) {
 | 
			
		||||
            // Prepare the data to pass to the component.
 | 
			
		||||
            this.data = {
 | 
			
		||||
                assign: this.assign,
 | 
			
		||||
                submission: this.submission,
 | 
			
		||||
                plugin: this.plugin,
 | 
			
		||||
                userId: this.userId,
 | 
			
		||||
                configs: AddonModAssignHelper.instance.getPluginConfig(this.assign, 'assignfeedback', this.plugin.type),
 | 
			
		||||
                edit: this.edit,
 | 
			
		||||
                canEdit: this.canEdit,
 | 
			
		||||
            };
 | 
			
		||||
        } else {
 | 
			
		||||
            // Data to render the plugin.
 | 
			
		||||
            this.text = AddonModAssign.instance.getSubmissionPluginText(this.plugin);
 | 
			
		||||
            this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin);
 | 
			
		||||
            this.notSupported = AddonModAssignFeedbackDelegate.instance.isPluginSupported(this.plugin.type);
 | 
			
		||||
            this.pluginLoaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open a modal to edit the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved with the input data, rejected if cancelled.
 | 
			
		||||
     */
 | 
			
		||||
    async editFeedback(): Promise<AddonModAssignFeedbackCommentsTextData> {
 | 
			
		||||
        if (!this.canEdit) {
 | 
			
		||||
            throw new CoreError('Cannot edit feedback');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create the navigation modal.
 | 
			
		||||
        const modal = await ModalController.instance.create({
 | 
			
		||||
            component: AddonModAssignEditFeedbackModalComponent,
 | 
			
		||||
            componentProps: {
 | 
			
		||||
                assign: this.assign,
 | 
			
		||||
                submission: this.submission,
 | 
			
		||||
                plugin: this.plugin,
 | 
			
		||||
                userId: this.userId,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await modal.present();
 | 
			
		||||
 | 
			
		||||
        const result = await modal.onDidDismiss();
 | 
			
		||||
 | 
			
		||||
        if (typeof result.data == 'undefined') {
 | 
			
		||||
            throw null; // User cancelled.
 | 
			
		||||
        } else {
 | 
			
		||||
            return result.data;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidate the plugin data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidate(): Promise<void> {
 | 
			
		||||
        await this.dynamicComponent.callComponentFunction('invalidate', []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AddonModAssignFeedbackPluginData = {
 | 
			
		||||
    assign: AddonModAssignAssign;
 | 
			
		||||
    submission: AddonModAssignSubmission;
 | 
			
		||||
    plugin: AddonModAssignPlugin;
 | 
			
		||||
    configs: AddonModAssignPluginConfig;
 | 
			
		||||
    edit: boolean;
 | 
			
		||||
    canEdit: boolean;
 | 
			
		||||
    userId: number;
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,142 @@
 | 
			
		||||
<!-- Buttons to add to the header. -->
 | 
			
		||||
<core-navbar-buttons slot="end">
 | 
			
		||||
    <core-context-menu>
 | 
			
		||||
        <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
 | 
			
		||||
            [href]="externalUrl" iconAction="fas-external-link-alt">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="assign && (description || (assign.introattachments && assign.introattachments.length))"
 | 
			
		||||
            [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()"
 | 
			
		||||
            iconAction="fas-arrow-right">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
 | 
			
		||||
            iconAction="far-newspaper" (action)="gotoBlog()">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
 | 
			
		||||
            (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="loaded && hasOffline && isOnline"  [priority]="600"
 | 
			
		||||
            [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon"
 | 
			
		||||
            [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
 | 
			
		||||
            [iconAction]="prefetchStatusIcon" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
 | 
			
		||||
            iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
    </core-context-menu>
 | 
			
		||||
</core-navbar-buttons>
 | 
			
		||||
 | 
			
		||||
<!-- Content. -->
 | 
			
		||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
 | 
			
		||||
 | 
			
		||||
    <!-- Description and intro attachments. -->
 | 
			
		||||
    <ion-card *ngIf="description" (click)="expandDescription($event)" class="core-clickable">
 | 
			
		||||
        <ion-item class="ion-text-wrap">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120"
 | 
			
		||||
                    contextLevel="module" [contextInstanceId]="module!.id" [courseId]="courseId" (click)="expandDescription($event)">
 | 
			
		||||
                </core-format-text>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-card>
 | 
			
		||||
 | 
			
		||||
    <ion-card *ngIf="assign && assign.introattachments && assign.introattachments.length">
 | 
			
		||||
        <core-file *ngFor="let file of assign.introattachments" [file]="file" [component]="component" [componentId]="componentId">
 | 
			
		||||
        </core-file>
 | 
			
		||||
    </ion-card>
 | 
			
		||||
 | 
			
		||||
    <!-- Assign has something offline. -->
 | 
			
		||||
    <ion-card class="core-warning-card" *ngIf="hasOffline">
 | 
			
		||||
        <ion-item>
 | 
			
		||||
            <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
            <ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-card>
 | 
			
		||||
 | 
			
		||||
    <!-- User can view all submissions (teacher). -->
 | 
			
		||||
    <ng-container *ngIf="assign && canViewAllSubmissions">
 | 
			
		||||
        <ion-list class="core-list-align-detail-right with-borders">
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="(groupInfo.separateGroups || groupInfo.visibleGroups)">
 | 
			
		||||
                <ion-label id="addon-assign-groupslabel">
 | 
			
		||||
                    <ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
 | 
			
		||||
                    <ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-assign-groupslabel"
 | 
			
		||||
                    interface="action-sheet">
 | 
			
		||||
                    <ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
 | 
			
		||||
                        {{groupOpt.name}}
 | 
			
		||||
                    </ion-select-option>
 | 
			
		||||
                </ion-select>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="timeRemaining">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
 | 
			
		||||
                    <p>{{ timeRemaining }}</p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="lateSubmissions">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2>{{ 'addon.mod_assign.latesubmissions' | translate }}</h2>
 | 
			
		||||
                    <p>{{ lateSubmissions }}</p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Summary of all submissions. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="summary && summary.participantcount" (click)="goToSubmissionList()" detail>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2 *ngIf="assign.teamsubmission">{{ 'addon.mod_assign.numberofteams' | translate }}</h2>
 | 
			
		||||
                    <h2 *ngIf="!assign.teamsubmission">{{ 'addon.mod_assign.numberofparticipants' | translate }}</h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-badge slot="end" *ngIf="showNumbers" color="primary">
 | 
			
		||||
                    {{ summary.participantcount }}
 | 
			
		||||
                </ion-badge>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Summary of submissions with draft status. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="assign.submissiondrafts && summary && summary.submissionsenabled"
 | 
			
		||||
                [detail]="!showNumbers || summary.submissiondraftscount"
 | 
			
		||||
                (click)="goToSubmissionList(submissionStatusDraft, !!summary.submissiondraftscount)">
 | 
			
		||||
                <ion-label><h2>{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</h2></ion-label>
 | 
			
		||||
                <ion-badge slot="end" *ngIf="showNumbers" color="primary">
 | 
			
		||||
                    {{ summary.submissiondraftscount }}
 | 
			
		||||
                </ion-badge>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Summary of submissions with submitted status. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="summary && summary.submissionsenabled"
 | 
			
		||||
                [detail]="!showNumbers || summary.submissionssubmittedcount"
 | 
			
		||||
                (click)="goToSubmissionList(submissionStatusSubmitted, !!summary.submissionssubmittedcount)">
 | 
			
		||||
                <ion-label><h2>{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</h2></ion-label>
 | 
			
		||||
                <ion-badge slot="end" *ngIf="showNumbers" color="primary">
 | 
			
		||||
                    {{ summary.submissionssubmittedcount }}
 | 
			
		||||
                </ion-badge>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Summary of submissions that need grading. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="summary && summary.submissionsenabled && !assign.teamsubmission && showNumbers"
 | 
			
		||||
                [detail]="needsGradingAvalaible"
 | 
			
		||||
                (click)="goToSubmissionList(needGrading, needsGradingAvalaible)">
 | 
			
		||||
                <ion-label><h2>{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}</h2></ion-label>
 | 
			
		||||
                <ion-badge slot="end" color="primary">
 | 
			
		||||
                    {{ summary.submissionsneedgradingcount }}
 | 
			
		||||
                </ion-badge>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
 | 
			
		||||
        <!-- Ungrouped users. -->
 | 
			
		||||
        <ion-card *ngIf="assign.teamsubmission && summary && summary.warnofungroupedusers" class="core-info-card">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-question-circle" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'addon.mod_assign.'+summary.warnofungroupedusers | translate }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <!-- If it's a student, display his submission. -->
 | 
			
		||||
    <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId"
 | 
			
		||||
        [moduleId]="module!.id">
 | 
			
		||||
    </addon-mod-assign-submission>
 | 
			
		||||
 | 
			
		||||
</core-loading>
 | 
			
		||||
							
								
								
									
										424
									
								
								src/addons/mod/assign/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										424
									
								
								src/addons/mod/assign/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,424 @@
 | 
			
		||||
// (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, Optional, OnDestroy, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { CoreSite } from '@classes/site';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignGradedEventData,
 | 
			
		||||
    AddonModAssignProvider,
 | 
			
		||||
    AddonModAssignSubmissionGradingSummary,
 | 
			
		||||
    AddonModAssignSubmissionSavedEventData,
 | 
			
		||||
    AddonModAssignSubmittedForGradingEventData,
 | 
			
		||||
} from '../../services/assign';
 | 
			
		||||
import { AddonModAssignOffline } from '../../services/assign-offline';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignAutoSyncData,
 | 
			
		||||
    AddonModAssignSync,
 | 
			
		||||
    AddonModAssignSyncProvider,
 | 
			
		||||
    AddonModAssignSyncResult,
 | 
			
		||||
} from '../../services/assign-sync';
 | 
			
		||||
import { AddonModAssignSubmissionComponent } from '../submission/submission';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays an assignment.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-assign-index',
 | 
			
		||||
    templateUrl: 'addon-mod-assign-index.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
   @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
 | 
			
		||||
 | 
			
		||||
    component = AddonModAssignProvider.COMPONENT;
 | 
			
		||||
    moduleName = 'assign';
 | 
			
		||||
 | 
			
		||||
    assign?: AddonModAssignAssign; // The assign object.
 | 
			
		||||
    canViewAllSubmissions = false; // Whether the user can view all submissions.
 | 
			
		||||
    canViewOwnSubmission = false; // Whether the user can view their own submission.
 | 
			
		||||
    timeRemaining?: string; // Message about time remaining to submit.
 | 
			
		||||
    lateSubmissions?: string; // Message about late submissions.
 | 
			
		||||
    showNumbers = true; // Whether to show number of submissions with each status.
 | 
			
		||||
    summary?: AddonModAssignSubmissionGradingSummary; // The grading summary.
 | 
			
		||||
    needsGradingAvalaible = false; // Whether we can see the submissions that need grading.
 | 
			
		||||
 | 
			
		||||
    groupInfo: CoreGroupInfo = {
 | 
			
		||||
        groups: [],
 | 
			
		||||
        separateGroups: false,
 | 
			
		||||
        visibleGroups: false,
 | 
			
		||||
        defaultGroupId: 0,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Status.
 | 
			
		||||
    submissionStatusSubmitted = AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED;
 | 
			
		||||
    submissionStatusDraft = AddonModAssignProvider.SUBMISSION_STATUS_DRAFT;
 | 
			
		||||
    needGrading = AddonModAssignProvider.NEED_GRADING;
 | 
			
		||||
 | 
			
		||||
    protected currentUserId?: number; // Current user ID.
 | 
			
		||||
    protected currentSite?: CoreSite; // Current user ID.
 | 
			
		||||
    protected syncEventName = AddonModAssignSyncProvider.AUTO_SYNCED;
 | 
			
		||||
 | 
			
		||||
    // Observers.
 | 
			
		||||
    protected savedObserver?: CoreEventObserver;
 | 
			
		||||
    protected submittedObserver?: CoreEventObserver;
 | 
			
		||||
    protected gradedObserver?: CoreEventObserver;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected content?: IonContent,
 | 
			
		||||
        @Optional() courseContentsPage?: CoreCourseContentsPage,
 | 
			
		||||
    ) {
 | 
			
		||||
        super('AddonModLessonIndexComponent', content, courseContentsPage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
 | 
			
		||||
        this.currentSite = CoreSites.instance.getCurrentSite();
 | 
			
		||||
 | 
			
		||||
        // Listen to events.
 | 
			
		||||
        this.savedObserver = CoreEvents.on<AddonModAssignSubmissionSavedEventData>(
 | 
			
		||||
            AddonModAssignProvider.SUBMISSION_SAVED_EVENT,
 | 
			
		||||
            (data) => {
 | 
			
		||||
                if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
 | 
			
		||||
                // Assignment submission saved, refresh data.
 | 
			
		||||
                    this.showLoadingAndRefresh(true, false);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.siteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.submittedObserver = CoreEvents.on<AddonModAssignSubmittedForGradingEventData>(
 | 
			
		||||
            AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT,
 | 
			
		||||
            (data) => {
 | 
			
		||||
                if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
 | 
			
		||||
                // Assignment submitted, check completion.
 | 
			
		||||
                    CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata);
 | 
			
		||||
 | 
			
		||||
                    // Reload data since it can have offline data now.
 | 
			
		||||
                    this.showLoadingAndRefresh(true, false);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            this.siteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.gradedObserver = CoreEvents.on<AddonModAssignGradedEventData>(AddonModAssignProvider.GRADED_EVENT, (data) => {
 | 
			
		||||
            if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
 | 
			
		||||
                // Assignment graded, refresh data.
 | 
			
		||||
                this.showLoadingAndRefresh(true, false);
 | 
			
		||||
            }
 | 
			
		||||
        }, this.siteId);
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModAssign.instance.logView(this.assign!.id, this.assign!.name);
 | 
			
		||||
            CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors. Just don't check Module completion.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.canViewAllSubmissions) {
 | 
			
		||||
            // User can see all submissions, log grading view.
 | 
			
		||||
            CoreUtils.instance.ignoreErrors(AddonModAssign.instance.logGradingView(this.assign!.id, this.assign!.name));
 | 
			
		||||
        } else if (this.canViewOwnSubmission) {
 | 
			
		||||
            // User can only see their own submission, log view the user submission.
 | 
			
		||||
            CoreUtils.instance.ignoreErrors(AddonModAssign.instance.logSubmissionView(this.assign!.id, this.assign!.name));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Expand the description.
 | 
			
		||||
     */
 | 
			
		||||
    expandDescription(ev?: Event): void {
 | 
			
		||||
        ev?.preventDefault();
 | 
			
		||||
        ev?.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        if (this.assign && (this.description || this.assign.introattachments)) {
 | 
			
		||||
            CoreTextUtils.instance.viewText(Translate.instance.instant('core.description'), this.description || '', {
 | 
			
		||||
                component: this.component,
 | 
			
		||||
                componentId: this.module!.id,
 | 
			
		||||
                files: this.assign.introattachments,
 | 
			
		||||
                filter: true,
 | 
			
		||||
                contextLevel: 'module',
 | 
			
		||||
                instanceId: this.module!.id,
 | 
			
		||||
                courseId: this.courseId,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get assignment data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresh If it's refreshing content.
 | 
			
		||||
     * @param sync If it should try to sync.
 | 
			
		||||
     * @param showErrors If show errors to the user of hide them.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        // Get assignment data.
 | 
			
		||||
        try {
 | 
			
		||||
            this.assign = await AddonModAssign.instance.getAssignment(this.courseId!, this.module!.id);
 | 
			
		||||
 | 
			
		||||
            this.dataRetrieved.emit(this.assign);
 | 
			
		||||
            this.description = this.assign.intro;
 | 
			
		||||
 | 
			
		||||
            if (sync) {
 | 
			
		||||
                // Try to synchronize the assign.
 | 
			
		||||
                await CoreUtils.instance.ignoreErrors(this.syncActivity(showErrors));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if there's any offline data for this assign.
 | 
			
		||||
            this.hasOffline = await AddonModAssignOffline.instance.hasAssignOfflineData(this.assign.id);
 | 
			
		||||
 | 
			
		||||
            // Get assignment submissions.
 | 
			
		||||
            const submissions = await AddonModAssign.instance.getSubmissions(this.assign.id, { cmId: this.module!.id });
 | 
			
		||||
            const time = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
 | 
			
		||||
            this.canViewAllSubmissions = submissions.canviewsubmissions;
 | 
			
		||||
 | 
			
		||||
            if (submissions.canviewsubmissions) {
 | 
			
		||||
 | 
			
		||||
                // Calculate the messages to display about time remaining and late submissions.
 | 
			
		||||
                if (this.assign.duedate > 0) {
 | 
			
		||||
                    if (this.assign.duedate - time <= 0) {
 | 
			
		||||
                        this.timeRemaining = Translate.instance.instant('addon.mod_assign.assignmentisdue');
 | 
			
		||||
                    } else {
 | 
			
		||||
                        this.timeRemaining = CoreTimeUtils.instance.formatDuration(this.assign.duedate - time, 3);
 | 
			
		||||
 | 
			
		||||
                        if (this.assign.cutoffdate) {
 | 
			
		||||
                            if (this.assign.cutoffdate > time) {
 | 
			
		||||
                                this.lateSubmissions = Translate.instance.instant(
 | 
			
		||||
                                    'addon.mod_assign.latesubmissionsaccepted',
 | 
			
		||||
                                    { $a: CoreTimeUtils.instance.userDate(this.assign.cutoffdate * 1000) },
 | 
			
		||||
                                );
 | 
			
		||||
                            } else {
 | 
			
		||||
                                this.lateSubmissions = Translate.instance.instant('addon.mod_assign.nomoresubmissionsaccepted');
 | 
			
		||||
                            }
 | 
			
		||||
                        } else {
 | 
			
		||||
                            this.lateSubmissions = '';
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.timeRemaining = '';
 | 
			
		||||
                    this.lateSubmissions = '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Check if groupmode is enabled to avoid showing wrong numbers.
 | 
			
		||||
                this.groupInfo = await CoreGroups.instance.getActivityGroupInfo(this.assign.cmid, false);
 | 
			
		||||
                this.showNumbers = (this.groupInfo.groups && this.groupInfo.groups.length == 0) ||
 | 
			
		||||
                    this.currentSite!.isVersionGreaterEqualThan('3.5');
 | 
			
		||||
 | 
			
		||||
                await this.setGroup(CoreGroups.instance.validateGroupId(this.group, this.groupInfo));
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                // Check if the user can view their own submission.
 | 
			
		||||
                await AddonModAssign.instance.getSubmissionStatus(this.assign.id, { cmId: this.module!.id });
 | 
			
		||||
                this.canViewOwnSubmission = true;
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                this.canViewOwnSubmission = false;
 | 
			
		||||
 | 
			
		||||
                if (error.errorcode !== 'nopermission') {
 | 
			
		||||
                    throw error;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.fillContextMenu(refresh);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set group to see the summary.
 | 
			
		||||
     *
 | 
			
		||||
     * @param groupId Group ID.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async setGroup(groupId = 0): Promise<void> {
 | 
			
		||||
        this.group = groupId;
 | 
			
		||||
 | 
			
		||||
        const submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign!.id, {
 | 
			
		||||
            groupId: this.group,
 | 
			
		||||
            cmId: this.module!.id,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        this.summary = submissionStatus.gradingsummary;
 | 
			
		||||
        if (!this.summary) {
 | 
			
		||||
            this.needsGradingAvalaible = false;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.summary?.warnofungroupedusers === true) {
 | 
			
		||||
            this.summary.warnofungroupedusers = 'ungroupedusers';
 | 
			
		||||
        } else {
 | 
			
		||||
            switch (this.summary?.warnofungroupedusers) {
 | 
			
		||||
                case AddonModAssignProvider.WARN_GROUPS_REQUIRED:
 | 
			
		||||
                    this.summary.warnofungroupedusers = 'ungroupedusers';
 | 
			
		||||
                    break;
 | 
			
		||||
                case AddonModAssignProvider.WARN_GROUPS_OPTIONAL:
 | 
			
		||||
                    this.summary.warnofungroupedusers = 'ungroupedusersoptional';
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    this.summary.warnofungroupedusers = '';
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.needsGradingAvalaible =
 | 
			
		||||
            (submissionStatus.gradingsummary?.submissionsneedgradingcount || 0) > 0 &&
 | 
			
		||||
            this.currentSite!.isVersionGreaterEqualThan('3.2');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Go to view a list of submissions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param status Status to see.
 | 
			
		||||
     * @param hasSubmissions If the status has any submission.
 | 
			
		||||
     */
 | 
			
		||||
    goToSubmissionList(status?: string, hasSubmissions = false): void {
 | 
			
		||||
        if (typeof status != 'undefined' && !hasSubmissions && this.showNumbers) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const params: Params = {
 | 
			
		||||
            groupId: this.group || 0,
 | 
			
		||||
            moduleName: this.moduleName,
 | 
			
		||||
        };
 | 
			
		||||
        if (typeof status != 'undefined') {
 | 
			
		||||
            params.status = status;
 | 
			
		||||
        }
 | 
			
		||||
        CoreNavigator.instance.navigate('submission', {
 | 
			
		||||
            params,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned by the sync function.
 | 
			
		||||
     * @return If succeed or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModAssignSyncResult): boolean {
 | 
			
		||||
        if (result.updated) {
 | 
			
		||||
            this.submissionComponent?.invalidateAndRefresh(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result.updated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform the invalidate content function.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async invalidateContent(): Promise<void> {
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId!));
 | 
			
		||||
 | 
			
		||||
        if (this.assign) {
 | 
			
		||||
            promises.push(AddonModAssign.instance.invalidateAllSubmissionData(this.assign.id));
 | 
			
		||||
 | 
			
		||||
            if (this.canViewAllSubmissions) {
 | 
			
		||||
                promises.push(AddonModAssign.instance.invalidateSubmissionStatusData(this.assign.id, undefined, this.group));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises).finally(() => {
 | 
			
		||||
            this.submissionComponent?.invalidateAndRefresh(true);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * User entered the page that contains the component.
 | 
			
		||||
     */
 | 
			
		||||
    ionViewDidEnter(): void {
 | 
			
		||||
        super.ionViewDidEnter();
 | 
			
		||||
 | 
			
		||||
        this.submissionComponent?.ionViewDidEnter();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * User left the page that contains the component.
 | 
			
		||||
     */
 | 
			
		||||
    ionViewDidLeave(): void {
 | 
			
		||||
        super.ionViewDidLeave();
 | 
			
		||||
 | 
			
		||||
        this.submissionComponent?.ionViewDidLeave();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Compares sync event data with current data to check if refresh content is needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param syncEventData Data receiven on sync observer.
 | 
			
		||||
     * @return True if refresh is needed, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected isRefreshSyncNeeded(syncEventData: AddonModAssignAutoSyncData): boolean {
 | 
			
		||||
        if (this.assign && syncEventData.assignId == this.assign.id) {
 | 
			
		||||
            if (syncEventData.warnings && syncEventData.warnings.length) {
 | 
			
		||||
                // Show warnings.
 | 
			
		||||
                CoreDomUtils.instance.showErrorModal(syncEventData.warnings[0]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async sync(): Promise<void> {
 | 
			
		||||
        await AddonModAssignSync.instance.syncAssign(this.assign!.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        super.ngOnDestroy();
 | 
			
		||||
 | 
			
		||||
        this.savedObserver?.off();
 | 
			
		||||
        this.submittedObserver?.off();
 | 
			
		||||
        this.gradedObserver?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,23 @@
 | 
			
		||||
 | 
			
		||||
<core-dynamic-component [component]="pluginComponent" [data]="data">
 | 
			
		||||
    <!-- This content will be replaced by the component if found. -->
 | 
			
		||||
    <core-loading [hideUntil]="pluginLoaded">
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="text.length > 0 || files.length > 0">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <h2>{{ plugin.name }}</h2>
 | 
			
		||||
                <ion-badge *ngIf="notSupported" color="primary">
 | 
			
		||||
                    {{ 'addon.mod_assign.submissionnotsupported' | translate }}
 | 
			
		||||
                </ion-badge>
 | 
			
		||||
                <p *ngIf="text">
 | 
			
		||||
                    <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true"
 | 
			
		||||
                        [fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid"
 | 
			
		||||
                        [courseId]="assign.course">
 | 
			
		||||
                    </core-format-text>
 | 
			
		||||
                </p>
 | 
			
		||||
                <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid"
 | 
			
		||||
                    [alwaysDownload]="true">
 | 
			
		||||
                </core-file>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</core-dynamic-component>
 | 
			
		||||
@ -0,0 +1,115 @@
 | 
			
		||||
// (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, Input, OnInit, Type, ViewChild } from '@angular/core';
 | 
			
		||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssignPlugin,
 | 
			
		||||
    AddonModAssignProvider,
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
} 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.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-assign-submission-plugin',
 | 
			
		||||
    templateUrl: 'addon-mod-assign-submission-plugin.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignSubmissionPluginComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(CoreDynamicComponent) dynamicComponent!: CoreDynamicComponent;
 | 
			
		||||
 | 
			
		||||
    @Input() assign!: AddonModAssignAssign; // The assignment.
 | 
			
		||||
    @Input() submission!: AddonModAssignSubmission; // The submission.
 | 
			
		||||
    @Input() plugin!: AddonModAssignPlugin; // The plugin object.
 | 
			
		||||
    @Input() edit = false; // Whether the user is editing.
 | 
			
		||||
    @Input() allowOffline = false; // Whether to allow offline.
 | 
			
		||||
 | 
			
		||||
    pluginComponent?: Type<unknown>; // Component to render the plugin.
 | 
			
		||||
    data?: AddonModAssignSubmissionPluginData; // Data to pass to the component.
 | 
			
		||||
 | 
			
		||||
    // Data to render the plugin if it isn't supported.
 | 
			
		||||
    component = AddonModAssignProvider.COMPONENT;
 | 
			
		||||
    text = '';
 | 
			
		||||
    files: (FileEntry | CoreWSExternalFile)[] = [];
 | 
			
		||||
    notSupported = false;
 | 
			
		||||
    pluginLoaded = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        if (!this.plugin) {
 | 
			
		||||
            this.pluginLoaded = true;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const name = AddonModAssignSubmissionDelegate.instance.getPluginName(this.plugin);
 | 
			
		||||
 | 
			
		||||
        if (!name) {
 | 
			
		||||
            this.pluginLoaded = true;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this.plugin.name = name;
 | 
			
		||||
 | 
			
		||||
        // Check if the plugin has defined its own component to render itself.
 | 
			
		||||
        this.pluginComponent = await AddonModAssignSubmissionDelegate.instance.getComponentForPlugin(this.plugin, this.edit);
 | 
			
		||||
 | 
			
		||||
        if (this.pluginComponent) {
 | 
			
		||||
            // Prepare the data to pass to the component.
 | 
			
		||||
            this.data = {
 | 
			
		||||
                assign: this.assign,
 | 
			
		||||
                submission: this.submission,
 | 
			
		||||
                plugin: this.plugin,
 | 
			
		||||
                configs: AddonModAssignHelper.instance.getPluginConfig(this.assign, 'assignsubmission', this.plugin.type),
 | 
			
		||||
                edit: this.edit,
 | 
			
		||||
                allowOffline: this.allowOffline,
 | 
			
		||||
            };
 | 
			
		||||
        } else {
 | 
			
		||||
            // Data to render the plugin.
 | 
			
		||||
            this.text = AddonModAssign.instance.getSubmissionPluginText(this.plugin);
 | 
			
		||||
            this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin);
 | 
			
		||||
            this.notSupported = AddonModAssignSubmissionDelegate.instance.isPluginSupported(this.plugin.type);
 | 
			
		||||
            this.pluginLoaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidate the plugin data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidate(): Promise<void> {
 | 
			
		||||
        await this.dynamicComponent.callComponentFunction('invalidate', []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AddonModAssignSubmissionPluginData = {
 | 
			
		||||
    assign: AddonModAssignAssign;
 | 
			
		||||
    submission: AddonModAssignSubmission;
 | 
			
		||||
    plugin: AddonModAssignPlugin;
 | 
			
		||||
    configs: AddonModAssignPluginConfig;
 | 
			
		||||
    edit: boolean;
 | 
			
		||||
    allowOffline: boolean;
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,395 @@
 | 
			
		||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
 | 
			
		||||
 | 
			
		||||
    <!-- User and status of the submission. -->
 | 
			
		||||
    <ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId"
 | 
			
		||||
        [title]="user!.fullname">
 | 
			
		||||
        <core-user-avatar [user]="user" slot="start"></core-user-avatar>
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <h2>{{ user!.fullname }}</h2>
 | 
			
		||||
            <ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
        <ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
 | 
			
		||||
    <!-- Status of the submission if user is blinded. -->
 | 
			
		||||
    <ion-item class="ion-text-wrap" *ngIf="blindMarking && !user">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <h2>{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</h2>
 | 
			
		||||
            <ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
        <ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
 | 
			
		||||
    <!-- Status of the submission in the rest of cases. -->
 | 
			
		||||
    <ion-item class="ion-text-wrap" *ngIf="(blindMarking && user) || (!blindMarking && !user)">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <h2>{{ 'addon.mod_assign.submissionstatus' | translate }}</h2>
 | 
			
		||||
            <ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
        <ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
 | 
			
		||||
    <!-- Tabs: see the submission or grade it. -->
 | 
			
		||||
    <core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true" (ionChange)="tabSelected($event)">
 | 
			
		||||
        <!-- View the submission tab. -->
 | 
			
		||||
        <core-tab [title]="'addon.mod_assign.submission' | translate" id="submission">
 | 
			
		||||
            <ng-template>
 | 
			
		||||
                <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins"
 | 
			
		||||
                    [assign]="assign" [submission]="userSubmission" [plugin]="plugin">
 | 
			
		||||
                </addon-mod-assign-submission-plugin>
 | 
			
		||||
 | 
			
		||||
                <!-- Render some data about the submission. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap"
 | 
			
		||||
                    *ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
 | 
			
		||||
                        <p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="timeRemaining" [ngClass]="[timeRemainingClass]">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
 | 
			
		||||
                        <p [innerHTML]="timeRemaining"></p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="fromDate && !isSubmittedForGrading">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <p *ngIf="assign!.intro"
 | 
			
		||||
                            [innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}">
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p *ngIf="!assign!.intro"
 | 
			
		||||
                            [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate:
 | 
			
		||||
                                {'$a': fromDate}">
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && !isSubmittedForGrading">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.duedate' | translate }}</h2>
 | 
			
		||||
                        <p *ngIf="assign!.duedate" >{{ assign!.duedate * 1000 | coreFormatDate }}</p>
 | 
			
		||||
                        <p *ngIf="!assign!.duedate" >{{ 'addon.mod_assign.duedateno' | translate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && assign!.cutoffdate && isSubmittedForGrading">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2>
 | 
			
		||||
                        <p>{{ assign!.cutoffdate * 1000 | coreFormatDate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <ion-item class="ion-text-wrap"
 | 
			
		||||
                    *ngIf="assign!.duedate && lastAttempt?.extensionduedate && !isSubmittedForGrading">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2>
 | 
			
		||||
                        <p>{{ lastAttempt!.extensionduedate * 1000 | coreFormatDate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
 | 
			
		||||
                        <p *ngIf="assign!.maxattempts == unlimitedAttempts">
 | 
			
		||||
                            {{ 'addon.mod_assign.outof' | translate :
 | 
			
		||||
                                {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p *ngIf="assign!.maxattempts != unlimitedAttempts">
 | 
			
		||||
                            {{ 'addon.mod_assign.outof' | translate :
 | 
			
		||||
                                {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <!-- Add or edit submission. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="canEdit">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <div *ngIf="!unsupportedEditPlugins.length && !showErrorStatementEdit">
 | 
			
		||||
                            <!-- If has offline data, show edit. -->
 | 
			
		||||
                            <ion-button expand="block" class="ion-text-wrap" color="primary" *ngIf="hasOffline"
 | 
			
		||||
                                (click)="goToEdit()">
 | 
			
		||||
                                {{ 'addon.mod_assign.editsubmission' | translate }}
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                            <!-- If no submission or is new, show add submission. -->
 | 
			
		||||
                            <ion-button expand="block" class="ion-text-wrap" color="primary"
 | 
			
		||||
                                *ngIf="!hasOffline &&
 | 
			
		||||
                                    (!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)"
 | 
			
		||||
                                (click)="goToEdit()">
 | 
			
		||||
                                {{ 'addon.mod_assign.addsubmission' | translate }}
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                            <!-- If reopened, show addfromprevious and addnewattempt. -->
 | 
			
		||||
                            <ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened">
 | 
			
		||||
                                <ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap" color="primary"
 | 
			
		||||
                                    (click)="copyPrevious()">
 | 
			
		||||
                                    {{ 'addon.mod_assign.addnewattemptfromprevious' | translate }}
 | 
			
		||||
                                </ion-button>
 | 
			
		||||
                                <ion-button expand="block" class="ion-text-wrap" color="primary" (click)="goToEdit()">
 | 
			
		||||
                                    {{ 'addon.mod_assign.addnewattempt' | translate }}
 | 
			
		||||
                                </ion-button>
 | 
			
		||||
                            </ng-container>
 | 
			
		||||
                            <!-- Else show editsubmission. -->
 | 
			
		||||
                            <ion-button expand="block" class="ion-text-wrap" color="primary"
 | 
			
		||||
                                *ngIf="!hasOffline && userSubmission && userSubmission!.status &&
 | 
			
		||||
                                    userSubmission!.status != statusNew &&
 | 
			
		||||
                                    userSubmission!.status != statusReopened" (click)="goToEdit()">
 | 
			
		||||
                                {{ 'addon.mod_assign.editsubmission' | translate }}
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div *ngIf="unsupportedEditPlugins && unsupportedEditPlugins.length && !showErrorStatementEdit">
 | 
			
		||||
                            <p class="core-danger-item">{{ 'addon.mod_assign.erroreditpluginsnotsupported' | translate }}</p>
 | 
			
		||||
                            <p class="core-danger-item" *ngFor="let name of unsupportedEditPlugins">{{ name }}</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div *ngIf="showErrorStatementEdit">
 | 
			
		||||
                            <p class="core-danger-item">{{ 'addon.mod_assign.cannoteditduetostatementsubmission' | translate }}</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <!-- Submit for grading form. -->
 | 
			
		||||
                <ng-container *ngIf="canSubmit">
 | 
			
		||||
                    <ion-item class="ion-text-wrap" *ngIf="submissionStatement">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <core-format-text [text]="submissionStatement" [filter]="false"></core-format-text>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                        <ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="acceptStatement">
 | 
			
		||||
                        </ion-checkbox>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <!-- Submit button. -->
 | 
			
		||||
                    <ion-item class="ion-text-wrap" *ngIf="!showErrorStatementSubmit">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <ion-button expand="block" class="ion-text-wrap"
 | 
			
		||||
                                (click)="submitForGrading(acceptStatement)">
 | 
			
		||||
                                {{ 'addon.mod_assign.submitassignment' | translate }}
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                            <p>{{ 'addon.mod_assign.submitassignment_help' | translate }}</p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <!-- Error because we lack submissions statement. -->
 | 
			
		||||
                    <ion-item class="ion-text-wrap" *ngIf="showErrorStatementSubmit">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <p class="core-danger-item">
 | 
			
		||||
                                {{ 'addon.mod_assign.cannotsubmitduetostatementsubmission' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
 | 
			
		||||
                <!-- Team members that need to submit it too. -->
 | 
			
		||||
                <ion-item-divider class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0">
 | 
			
		||||
                    <ion-label><h2>{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}</h2></ion-label>
 | 
			
		||||
                </ion-item-divider>
 | 
			
		||||
                <ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && !blindMarking">
 | 
			
		||||
                    <ng-container *ngFor="let user of membersToSubmit">
 | 
			
		||||
                        <ion-item class="ion-text-wrap" core-user-link [userId]="user.id"
 | 
			
		||||
                            [courseId]="courseId" [title]="user.fullname">
 | 
			
		||||
                            <core-user-avatar [user]="user" slot="start"></core-user-avatar>
 | 
			
		||||
                            <ion-label><h2>{{ user.fullname }}</h2></ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
                <ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && blindMarking">
 | 
			
		||||
                    <ng-container *ngFor="let blindId of membersToSubmitBlind">
 | 
			
		||||
                        <ion-item class="ion-text-wrap">
 | 
			
		||||
                            <ion-label>{{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }}</ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
 | 
			
		||||
                <!-- Submission is locked. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked">
 | 
			
		||||
                    <ion-label><h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2></ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <!-- Editing status. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap"
 | 
			
		||||
                    *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined"
 | 
			
		||||
                    [ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2>
 | 
			
		||||
                        <p *ngIf="lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p>
 | 
			
		||||
                        <p *ngIf="!lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
        </core-tab>
 | 
			
		||||
 | 
			
		||||
        <!-- Grade the submission tab. -->
 | 
			
		||||
        <core-tab [title]="'addon.mod_assign.grade' | translate" *ngIf="feedback || isGrading" id="grade">
 | 
			
		||||
            <ng-template>
 | 
			
		||||
                <!-- Current grade if method is advanced. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap core-grading-summary"
 | 
			
		||||
                    *ngIf="feedback?.gradefordisplay && (!isGrading || grade.method != 'simple')">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
 | 
			
		||||
                        <p><core-format-text [text]="feedback!.gradefordisplay" [filter]="false"></core-format-text></p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-button slot="end" *ngIf="feedback!.advancedgrade" (click)="showAdvancedGrade()">
 | 
			
		||||
                        <ion-icon name="fas-search" slot="icon-only"></ion-icon>
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <ng-container *ngIf="isGrading">
 | 
			
		||||
                    <!-- Numeric grade.
 | 
			
		||||
                        Use a text input because otherwise we cannot readthe value if it has an invalid character. -->
 | 
			
		||||
                    <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && !grade.scale">
 | 
			
		||||
                        <ion-label position="stacked">
 | 
			
		||||
                            <h2>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</h2>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                        <ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade"
 | 
			
		||||
                            [lang]="grade.lang">
 | 
			
		||||
                        </ion-input>
 | 
			
		||||
                        <p item-content *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
 | 
			
		||||
                    <!-- Grade using a scale. -->
 | 
			
		||||
                    <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && grade.scale">
 | 
			
		||||
                        <ion-label><h2>{{ 'addon.mod_assign.grade' | translate }}</h2></ion-label>
 | 
			
		||||
                        <ion-select [(ngModel)]="grade.grade" interface="action-sheet" [disabled]="grade.disabled">
 | 
			
		||||
                            <ion-select-option *ngFor="let grade of grade.scale" [value]="grade.value">
 | 
			
		||||
                                {{grade.label}}
 | 
			
		||||
                            </ion-select-option>
 | 
			
		||||
                        </ion-select>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
 | 
			
		||||
                    <!-- Outcomes. -->
 | 
			
		||||
                    <ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes">
 | 
			
		||||
                        <ion-label><h2>{{ outcome.name }}</h2></ion-label>
 | 
			
		||||
                        <ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId"
 | 
			
		||||
                            interface="action-sheet" [disabled]="gradeInfo!.disabled">
 | 
			
		||||
                            <ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value">
 | 
			
		||||
                                {{grade.label}}
 | 
			
		||||
                            </ion-select-option>
 | 
			
		||||
                        </ion-select>
 | 
			
		||||
                        <p item-content *ngIf="!canSaveGrades || !outcome.itemNumber">{{ outcome.selected }}</p>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
 | 
			
		||||
                    <!-- Gradebook grade for simple grading. -->
 | 
			
		||||
                    <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple'">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
 | 
			
		||||
                            <p *ngIf="grade.gradebookGrade && !grade.scale">
 | 
			
		||||
                                {{ grade.gradebookGrade }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="grade.gradebookGrade && grade.scale">
 | 
			
		||||
                                {{ grade.scale[grade.gradebookGrade].label }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="!grade.gradebookGrade">-</p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
 | 
			
		||||
                <addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback!.plugins" [assign]="assign"
 | 
			
		||||
                    [submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades">
 | 
			
		||||
                </addon-mod-assign-feedback-plugin>
 | 
			
		||||
 | 
			
		||||
                <!-- Workflow status. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="workflowStatusTranslationId">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.markingworkflowstate' | translate }}</h2>
 | 
			
		||||
                        <p>{{ workflowStatusTranslationId | translate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <!--- Apply grade to all team members. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2>
 | 
			
		||||
                        <p>{{ 'addon.mod_assign.applytoteam' | translate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <!-- Attempt status. -->
 | 
			
		||||
                <ng-container *ngIf="isGrading && assign!.attemptreopenmethod != attemptReopenMethodNone">
 | 
			
		||||
                    <ion-item class="ion-text-wrap">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2>
 | 
			
		||||
                            <p *ngIf="assign!.maxattempts == unlimitedAttempts">
 | 
			
		||||
                                {{ 'addon.mod_assign.outof' | translate :
 | 
			
		||||
                                    {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="assign!.maxattempts != unlimitedAttempts">
 | 
			
		||||
                                {{ 'addon.mod_assign.outof' | translate :
 | 
			
		||||
                                    {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p>
 | 
			
		||||
                                {{ 'addon.mod_assign.attemptreopenmethod' | translate }}:
 | 
			
		||||
                                {{ 'addon.mod_assign.attemptreopenmethod_' + assign!.attemptreopenmethod | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item *ngIf="canSaveGrades && allowAddAttempt" >
 | 
			
		||||
                        <ion-label>{{ 'addon.mod_assign.addattempt' | translate }}</ion-label>
 | 
			
		||||
                        <ion-toggle [(ngModel)]="grade.addAttempt"></ion-toggle>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
 | 
			
		||||
                <!-- Data about the grader (teacher who graded). -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader!.id" [courseId]="courseId"
 | 
			
		||||
                    [title]="grader!.fullname" detail="true">
 | 
			
		||||
                    <core-user-avatar [user]="grader" slot="start"></core-user-avatar>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2>
 | 
			
		||||
                        <h2>{{ grader!.fullname }}</h2>
 | 
			
		||||
                        <p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <!-- Grader is hidden, display only the grade date. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="!grader && feedback!.gradeddate">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_assign.gradedon' | translate }}</h2>
 | 
			
		||||
                        <p>{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <!-- Warning message if cannot save grades. -->
 | 
			
		||||
                <ion-card *ngIf="isGrading && !canSaveGrades" class="core-warning-card">
 | 
			
		||||
                    <ion-item>
 | 
			
		||||
                        <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p>
 | 
			
		||||
                            <ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link >
 | 
			
		||||
                                {{ 'core.openinbrowser' | translate }}
 | 
			
		||||
                                <ion-icon name="fas-external-link-alt" slot="end"></ion-icon>
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ion-card>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
        </core-tab>
 | 
			
		||||
    </core-tabs>
 | 
			
		||||
</core-loading>
 | 
			
		||||
 | 
			
		||||
<!-- Template to render some data regarding the submission status. -->
 | 
			
		||||
<ng-template #submissionStatus>
 | 
			
		||||
    <ng-container *ngIf="assign && assign!.teamsubmission && lastAttempt">
 | 
			
		||||
        <p *ngIf="lastAttempt!.submissiongroup && lastAttempt!.submissiongroupname">{{lastAttempt!.submissiongroupname}}</p>
 | 
			
		||||
        <ng-container *ngIf="assign!.preventsubmissionnotingroup &&
 | 
			
		||||
            !lastAttempt!.submissiongroup &&
 | 
			
		||||
            (!lastAttempt!.usergroups || lastAttempt!.usergroups.length <= 0)">
 | 
			
		||||
            <p class="text-danger"><strong>{{ 'addon.mod_assign.noteam' | translate }}</strong></p>
 | 
			
		||||
            <p class="text-danger">{{ 'addon.mod_assign.noteam_desc' | translate }}</p>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <ng-container *ngIf="assign!.preventsubmissionnotingroup &&
 | 
			
		||||
            !lastAttempt!.submissiongroup &&
 | 
			
		||||
            lastAttempt!.usergroups &&
 | 
			
		||||
            lastAttempt!.usergroups.length > 1">
 | 
			
		||||
            <p class="text-danger"><strong>{{ 'addon.mod_assign.multipleteams' | translate }}</strong></p>
 | 
			
		||||
            <p class="text-danger">{{ 'addon.mod_assign.multipleteams_desc' | translate }}</p>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <p *ngIf="!assign!.preventsubmissionnotingroup && !lastAttempt!.submissiongroup">
 | 
			
		||||
            {{ 'addon.mod_assign.defaultteam' | translate }}
 | 
			
		||||
        </p>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
</ng-template>
 | 
			
		||||
<ng-template #submissionStatusBadges>
 | 
			
		||||
    <ion-badge slot="end" *ngIf="statusTranslated" [color]="statusColor">
 | 
			
		||||
        {{ statusTranslated }}
 | 
			
		||||
    </ion-badge>
 | 
			
		||||
    <ion-badge slot="end" *ngIf="gradingStatusTranslationId" [color]="gradingColor">
 | 
			
		||||
        {{ gradingStatusTranslationId | translate }}
 | 
			
		||||
    </ion-badge>
 | 
			
		||||
</ng-template>
 | 
			
		||||
							
								
								
									
										30
									
								
								src/addons/mod/assign/components/submission/submission.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/addons/mod/assign/components/submission/submission.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
:host ::ng-deep {
 | 
			
		||||
    div.latesubmission,
 | 
			
		||||
    div.overdue {
 | 
			
		||||
        border-bottom: 3px solid var(--danger) !important;
 | 
			
		||||
        ion-icon {
 | 
			
		||||
            color: var(--danger);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    div.earlysubmission {
 | 
			
		||||
        border-bottom: 3px solid var(--success) !important;
 | 
			
		||||
        ion-icon {
 | 
			
		||||
            color: var(--success);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    div.submissioneditable p {
 | 
			
		||||
        color: var(--red);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-grading-summary .advancedgrade {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(body.dark) ::ng-deep {
 | 
			
		||||
    div.submissioneditable p {
 | 
			
		||||
        color: var(--red-light);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1225
									
								
								src/addons/mod/assign/components/submission/submission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1225
									
								
								src/addons/mod/assign/components/submission/submission.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										47
									
								
								src/addons/mod/assign/feedback/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/addons/mod/assign/feedback/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
// (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 { AddonModAssignFeedbackCommentsHandler } from './services/handler';
 | 
			
		||||
import { AddonModAssignFeedbackCommentsComponent } from './component/comments';
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
 | 
			
		||||
import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModAssignFeedbackCommentsComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
        CoreEditorComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => () => {
 | 
			
		||||
                AddonModAssignFeedbackDelegate.instance.registerHandler(AddonModAssignFeedbackCommentsHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModAssignFeedbackCommentsComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    entryComponents: [
 | 
			
		||||
        AddonModAssignFeedbackCommentsComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignFeedbackCommentsModule {}
 | 
			
		||||
@ -0,0 +1,33 @@
 | 
			
		||||
<!-- Read only. -->
 | 
			
		||||
<ion-item class="ion-text-wrap" *ngIf="(text || canEdit) && !edit">
 | 
			
		||||
    <ion-label>
 | 
			
		||||
        <h2>{{ plugin.name }}</h2>
 | 
			
		||||
        <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>
 | 
			
		||||
    <div slot="end">
 | 
			
		||||
        <div class="ion-text-end">
 | 
			
		||||
            <ion-button fill="clear" *ngIf="canEdit" (click)="editComment()" color="dark">
 | 
			
		||||
                <ion-icon name="fas-pen" slot="icon-only"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ion-note *ngIf="!isSent" color="dark">
 | 
			
		||||
            <ion-icon name="far-clock"></ion-icon>
 | 
			
		||||
            {{ 'core.notsent' | translate }}
 | 
			
		||||
        </ion-note>
 | 
			
		||||
    </div>
 | 
			
		||||
</ion-item>
 | 
			
		||||
 | 
			
		||||
<!-- Edit -->
 | 
			
		||||
<ion-item class="ion-text-wrap" *ngIf="edit && loaded">
 | 
			
		||||
    <ion-label></ion-label>
 | 
			
		||||
    <core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name"
 | 
			
		||||
        name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true"
 | 
			
		||||
        contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor"
 | 
			
		||||
        [draftExtraParams]="{userid: userId, action: 'grade'}">
 | 
			
		||||
    </core-rich-text-editor>
 | 
			
		||||
</ion-item>
 | 
			
		||||
							
								
								
									
										161
									
								
								src/addons/mod/assign/feedback/comments/component/comments.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/addons/mod/assign/feedback/comments/component/comments.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,161 @@
 | 
			
		||||
// (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, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormControl } from '@angular/forms';
 | 
			
		||||
import { AddonModAssignFeedbackPluginComponent } from '@addons/mod/assign/components/feedback-plugin/feedback-plugin';
 | 
			
		||||
import { AddonModAssign, AddonModAssignProvider } from '@addons/mod/assign/services/assign';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignFeedbackCommentsDraftData,
 | 
			
		||||
    AddonModAssignFeedbackCommentsHandler,
 | 
			
		||||
    AddonModAssignFeedbackCommentsPluginData,
 | 
			
		||||
} from '../services/handler';
 | 
			
		||||
import { AddonModAssignFeedbackDelegate } from '@addons/mod/assign/services/feedback-delegate';
 | 
			
		||||
import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
/**
 | 
			
		||||
 * Component to render a comments feedback plugin.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-assign-feedback-comments',
 | 
			
		||||
    templateUrl: 'addon-mod-assign-feedback-comments.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedbackPluginComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    control?: FormControl;
 | 
			
		||||
    component = AddonModAssignProvider.COMPONENT;
 | 
			
		||||
    text = '';
 | 
			
		||||
    isSent = false;
 | 
			
		||||
    loaded = false;
 | 
			
		||||
 | 
			
		||||
    protected element: HTMLElement;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        element: ElementRef,
 | 
			
		||||
        protected fb: FormBuilder,
 | 
			
		||||
    ) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.element = element.nativeElement;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            this.text = await this.getText();
 | 
			
		||||
 | 
			
		||||
            if (!this.canEdit && !this.edit) {
 | 
			
		||||
                // User cannot edit the comment. Show it full when clicked.
 | 
			
		||||
                this.element.addEventListener('click', (e) => {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
                    if (this.text) {
 | 
			
		||||
                        // Open a new state with the text.
 | 
			
		||||
                        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 if (this.edit) {
 | 
			
		||||
                this.control = this.fb.control(this.text);
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Edit the comment.
 | 
			
		||||
     */
 | 
			
		||||
    async editComment(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            const inputData = await this.editFeedback();
 | 
			
		||||
            const text = AddonModAssignFeedbackCommentsHandler.instance.getTextFromInputData(this.plugin, inputData);
 | 
			
		||||
 | 
			
		||||
            // Update the text and save it as draft.
 | 
			
		||||
            this.isSent = false;
 | 
			
		||||
            this.text = this.replacePluginfileUrls(text);
 | 
			
		||||
            AddonModAssignFeedbackDelegate.instance.saveFeedbackDraft(this.assign.id, this.userId, this.plugin, {
 | 
			
		||||
                text: text,
 | 
			
		||||
                format: 1,
 | 
			
		||||
            });
 | 
			
		||||
        } catch {
 | 
			
		||||
            // User cancelled, nothing to do.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the text for the plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved with the text.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getText(): Promise<string> {
 | 
			
		||||
        // Check if the user already modified the comment.
 | 
			
		||||
        const draft: AddonModAssignFeedbackCommentsDraftData | undefined =
 | 
			
		||||
            await AddonModAssignFeedbackDelegate.instance.getPluginDraftData(this.assign.id, this.userId, this.plugin);
 | 
			
		||||
 | 
			
		||||
        if (draft) {
 | 
			
		||||
            this.isSent = false;
 | 
			
		||||
 | 
			
		||||
            return this.replacePluginfileUrls(draft.text);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // There is no draft saved. Check if we have anything offline.
 | 
			
		||||
        const offlineData = await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
            AddonModAssignOffline.instance.getSubmissionGrade(this.assign.id, this.userId),
 | 
			
		||||
            undefined,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (offlineData && offlineData.plugindata && offlineData.plugindata.assignfeedbackcomments_editor) {
 | 
			
		||||
            const pluginData = <AddonModAssignFeedbackCommentsPluginData>offlineData.plugindata;
 | 
			
		||||
 | 
			
		||||
            // Save offline as draft.
 | 
			
		||||
            this.isSent = false;
 | 
			
		||||
            AddonModAssignFeedbackDelegate.instance.saveFeedbackDraft(
 | 
			
		||||
                this.assign.id,
 | 
			
		||||
                this.userId,
 | 
			
		||||
                this.plugin,
 | 
			
		||||
                pluginData.assignfeedbackcomments_editor,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return this.replacePluginfileUrls(pluginData.assignfeedbackcomments_editor.text);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // No offline data found, return online text.
 | 
			
		||||
        this.isSent = true;
 | 
			
		||||
 | 
			
		||||
        return AddonModAssign.instance.getSubmissionPluginText(this.plugin);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Replace @@PLUGINFILE@@ wildcards with the real URL of embedded files.
 | 
			
		||||
     *
 | 
			
		||||
     * @param Text to treat.
 | 
			
		||||
     * @return Treated text.
 | 
			
		||||
     */
 | 
			
		||||
    replacePluginfileUrls(text: string): string {
 | 
			
		||||
        const files = this.plugin.fileareas && this.plugin.fileareas[0] && this.plugin.fileareas[0].files;
 | 
			
		||||
 | 
			
		||||
        return CoreTextUtils.instance.replacePluginfileUrls(text, files || []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/feedback/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/feedback/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
    "pluginname": "Feedback comments"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										268
									
								
								src/addons/mod/assign/feedback/comments/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								src/addons/mod/assign/feedback/comments/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,268 @@
 | 
			
		||||
// (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 {
 | 
			
		||||
    AddonModAssignPlugin,
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
    AddonModAssignSavePluginData,
 | 
			
		||||
} from '@addons/mod/assign/services/assign';
 | 
			
		||||
import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline';
 | 
			
		||||
import { AddonModAssignFeedbackHandler } from '@addons/mod/assign/services/feedback-delegate';
 | 
			
		||||
import { Injectable, Type } from '@angular/core';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModAssignFeedbackCommentsComponent } from '../component/comments';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler for comments feedback plugin.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable( { providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignFeedbackCommentsHandlerService implements AddonModAssignFeedbackHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssignFeedbackCommentsHandler';
 | 
			
		||||
    type = 'comments';
 | 
			
		||||
 | 
			
		||||
    // Store the data in this service so it isn't lost if the user performs a PTR in the page.
 | 
			
		||||
    protected drafts: { [draftId: string]: AddonModAssignFeedbackCommentsDraftData } = {};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the text to submit.
 | 
			
		||||
     *
 | 
			
		||||
     * @param textUtils Text utils instance.
 | 
			
		||||
     * @param plugin Plugin.
 | 
			
		||||
     * @param inputData Data entered in the feedback edit form.
 | 
			
		||||
     * @return Text to submit.
 | 
			
		||||
     */
 | 
			
		||||
    getTextFromInputData(plugin: AddonModAssignPlugin, inputData: AddonModAssignFeedbackCommentsTextData): string {
 | 
			
		||||
        const files = plugin.fileareas && plugin.fileareas[0] ? plugin.fileareas[0].files : [];
 | 
			
		||||
 | 
			
		||||
        // The input data can have a string or an object with text and format. Get the text.
 | 
			
		||||
        const text = inputData.assignfeedbackcomments_editor || '';
 | 
			
		||||
 | 
			
		||||
        return CoreTextUtils.instance.restorePluginfileUrls(text, files || []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Discard the draft data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    discardDraft(assignId: number, userId: number, siteId?: string): void {
 | 
			
		||||
        const id = this.getDraftId(assignId, userId, siteId);
 | 
			
		||||
        if (typeof this.drafts[id] != 'undefined') {
 | 
			
		||||
            delete this.drafts[id];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the Component to use to display the plugin data.
 | 
			
		||||
     * 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 AddonModAssignFeedbackCommentsComponent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the draft saved data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Data (or promise resolved with the data).
 | 
			
		||||
     */
 | 
			
		||||
    getDraft(assignId: number, userId: number, siteId?: string): AddonModAssignFeedbackCommentsDraftData | undefined {
 | 
			
		||||
        const id = this.getDraftId(assignId, userId, siteId);
 | 
			
		||||
 | 
			
		||||
        if (typeof this.drafts[id] != 'undefined') {
 | 
			
		||||
            return this.drafts[id];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a draft ID.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Draft ID.
 | 
			
		||||
     */
 | 
			
		||||
    protected getDraftId(assignId: number, userId: number, siteId?: string): string {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        return siteId + '#' + assignId + '#' + userId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the feedback 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 feedback.
 | 
			
		||||
     * @param userId User ID of the submission.
 | 
			
		||||
     * @return Boolean (or promise resolved with boolean): whether the data has changed.
 | 
			
		||||
     */
 | 
			
		||||
    async hasDataChanged(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: AddonModAssignFeedbackCommentsTextData,
 | 
			
		||||
        userId: number,
 | 
			
		||||
    ): Promise<boolean> {
 | 
			
		||||
        // Get it from plugin or offline.
 | 
			
		||||
        const offlineData = await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
            AddonModAssignOffline.instance.getSubmissionGrade(assign.id, userId),
 | 
			
		||||
            undefined,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (offlineData?.plugindata?.assignfeedbackcomments_editor) {
 | 
			
		||||
            const pluginData = <AddonModAssignFeedbackCommentsPluginData>offlineData.plugindata;
 | 
			
		||||
 | 
			
		||||
            return !!pluginData.assignfeedbackcomments_editor.text;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // No offline data found, get text from plugin.
 | 
			
		||||
        const initialText = AddonModAssign.instance.getSubmissionPluginText(plugin);
 | 
			
		||||
        const newText = AddonModAssignFeedbackCommentsHandler.instance.getTextFromInputData(plugin, inputData);
 | 
			
		||||
 | 
			
		||||
        if (typeof newText == 'undefined') {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if text has changed.
 | 
			
		||||
        return initialText != newText;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether the plugin has draft data stored.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Boolean or promise resolved with boolean: whether the plugin has draft data.
 | 
			
		||||
     */
 | 
			
		||||
    hasDraftData(assignId: number, userId: number, siteId?: string): boolean | Promise<boolean> {
 | 
			
		||||
        const draft = this.getDraft(assignId, userId, siteId);
 | 
			
		||||
 | 
			
		||||
        return !!draft;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether or not the handler is enabled on a site level.
 | 
			
		||||
     *
 | 
			
		||||
     * @return True or promise resolved with true if enabled.
 | 
			
		||||
     */
 | 
			
		||||
    async isEnabled(): Promise<boolean> {
 | 
			
		||||
        // In here we should check if comments is not disabled in site.
 | 
			
		||||
        // But due to this is not a common comments place and it can be disabled separately into Moodle (disabling the plugin).
 | 
			
		||||
        // We are leaving it always enabled. It's also a teacher's feature.
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and add to pluginData the data to send to the server based on the draft data saved.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @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.
 | 
			
		||||
     */
 | 
			
		||||
    prepareFeedbackData(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void {
 | 
			
		||||
 | 
			
		||||
        const draft = this.getDraft(assignId, userId, siteId);
 | 
			
		||||
 | 
			
		||||
        if (draft) {
 | 
			
		||||
            // Add some HTML to the text if needed.
 | 
			
		||||
            draft.text = CoreTextUtils.instance.formatHtmlLines(draft.text);
 | 
			
		||||
 | 
			
		||||
            pluginData.assignfeedbackcomments_editor = draft;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save draft data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param data The data to save.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    saveDraft(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        data: AddonModAssignFeedbackCommentsDraftData,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void {
 | 
			
		||||
 | 
			
		||||
        if (data) {
 | 
			
		||||
            this.drafts[this.getDraftId(assignId, userId, siteId)] = data;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignFeedbackCommentsHandler = makeSingleton(AddonModAssignFeedbackCommentsHandlerService);
 | 
			
		||||
 | 
			
		||||
export type AddonModAssignFeedbackCommentsTextData = {
 | 
			
		||||
    // The text for this submission.
 | 
			
		||||
    assignfeedbackcomments_editor: string; // eslint-disable-line @typescript-eslint/naming-convention
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonModAssignFeedbackCommentsDraftData = {
 | 
			
		||||
    text: string; // The text for this feedback.
 | 
			
		||||
    format: number; // The format for this feedback.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonModAssignFeedbackCommentsPluginData = {
 | 
			
		||||
    // Editor structure.
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/naming-convention
 | 
			
		||||
    assignfeedbackcomments_editor: AddonModAssignFeedbackCommentsDraftData;
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,11 @@
 | 
			
		||||
<!-- Read only. -->
 | 
			
		||||
<ion-item class="ion-text-wrap" *ngIf="files && files.length">
 | 
			
		||||
    <ion-label>
 | 
			
		||||
        <h2>{{plugin.name}}</h2>
 | 
			
		||||
        <ng-container>
 | 
			
		||||
            <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid"
 | 
			
		||||
                [alwaysDownload]="true">
 | 
			
		||||
            </core-file>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
    </ion-label>
 | 
			
		||||
</ion-item>
 | 
			
		||||
							
								
								
									
										41
									
								
								src/addons/mod/assign/feedback/editpdf/component/editpdf.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/addons/mod/assign/feedback/editpdf/component/editpdf.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
// (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 { AddonModAssignFeedbackPluginComponent } from '@addons/mod/assign/components/feedback-plugin/feedback-plugin';
 | 
			
		||||
import { AddonModAssignProvider, AddonModAssign } from '@addons/mod/assign/services/assign';
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to render a edit pdf feedback plugin.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-assign-feedback-edit-pdf',
 | 
			
		||||
    templateUrl: 'addon-mod-assign-feedback-editpdf.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignFeedbackEditPdfComponent extends AddonModAssignFeedbackPluginComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    component = AddonModAssignProvider.COMPONENT;
 | 
			
		||||
    files: CoreWSExternalFile[] = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        if (this.plugin) {
 | 
			
		||||
            this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								src/addons/mod/assign/feedback/editpdf/editpdf.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addons/mod/assign/feedback/editpdf/editpdf.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
// (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 { AddonModAssignFeedbackEditPdfHandler } from './services/handler';
 | 
			
		||||
import { AddonModAssignFeedbackEditPdfComponent } from './component/editpdf';
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModAssignFeedbackEditPdfComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => () => {
 | 
			
		||||
                AddonModAssignFeedbackDelegate.instance.registerHandler(AddonModAssignFeedbackEditPdfHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModAssignFeedbackEditPdfComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    entryComponents: [
 | 
			
		||||
        AddonModAssignFeedbackEditPdfComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignFeedbackEditPdfModule {}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/feedback/editpdf/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/feedback/editpdf/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
    "pluginname": "Annotate PDF"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								src/addons/mod/assign/feedback/editpdf/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/addons/mod/assign/feedback/editpdf/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,73 @@
 | 
			
		||||
// (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 {
 | 
			
		||||
    AddonModAssignPlugin,
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
} from '@addons/mod/assign/services/assign';
 | 
			
		||||
import { AddonModAssignFeedbackHandler } from '@addons/mod/assign/services/feedback-delegate';
 | 
			
		||||
import { Injectable, Type } from '@angular/core';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModAssignFeedbackEditPdfComponent } from '../component/editpdf';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler for edit pdf feedback plugin.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable( { providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignFeedbackEditPdfHandlerService implements AddonModAssignFeedbackHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssignFeedbackEditPdfHandler';
 | 
			
		||||
    type = 'editpdf';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the Component to use to display the plugin data.
 | 
			
		||||
     * 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 AddonModAssignFeedbackEditPdfComponent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignFeedbackEditPdfHandler = makeSingleton(AddonModAssignFeedbackEditPdfHandlerService);
 | 
			
		||||
							
								
								
									
										27
									
								
								src/addons/mod/assign/feedback/feedback.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/addons/mod/assign/feedback/feedback.module.ts
									
									
									
									
									
										Normal 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 { AddonModAssignFeedbackCommentsModule } from './comments/comments.module';
 | 
			
		||||
import { AddonModAssignFeedbackEditPdfModule } from './editpdf/editpdf.module';
 | 
			
		||||
import { AddonModAssignFeedbackFileModule } from './file/file.module';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        AddonModAssignFeedbackCommentsModule,
 | 
			
		||||
        AddonModAssignFeedbackEditPdfModule,
 | 
			
		||||
        AddonModAssignFeedbackFileModule,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignFeedbackModule { }
 | 
			
		||||
@ -0,0 +1,11 @@
 | 
			
		||||
<!-- Read only. -->
 | 
			
		||||
<ion-item class="ion-text-wrap" *ngIf="files && files.length">
 | 
			
		||||
    <ion-label>
 | 
			
		||||
        <h2>{{plugin.name}}</h2>
 | 
			
		||||
        <ng-container>
 | 
			
		||||
            <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid"
 | 
			
		||||
                [alwaysDownload]="true">
 | 
			
		||||
            </core-file>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
    </ion-label>
 | 
			
		||||
</ion-item>
 | 
			
		||||
							
								
								
									
										41
									
								
								src/addons/mod/assign/feedback/file/component/file.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/addons/mod/assign/feedback/file/component/file.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
// (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 { AddonModAssignFeedbackPluginComponent } from '@addons/mod/assign/components/feedback-plugin/feedback-plugin';
 | 
			
		||||
import { AddonModAssign, AddonModAssignProvider } from '@addons/mod/assign/services/assign';
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to render a file feedback plugin.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-assign-feedback-file',
 | 
			
		||||
    templateUrl: 'addon-mod-assign-feedback-file.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignFeedbackFileComponent extends AddonModAssignFeedbackPluginComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    component = AddonModAssignProvider.COMPONENT;
 | 
			
		||||
    files: CoreWSExternalFile[] = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        if (this.plugin) {
 | 
			
		||||
            this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								src/addons/mod/assign/feedback/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addons/mod/assign/feedback/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
// (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 { AddonModAssignFeedbackFileHandler } from './services/handler';
 | 
			
		||||
import { AddonModAssignFeedbackFileComponent } from './component/file';
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModAssignFeedbackFileComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => () => {
 | 
			
		||||
                AddonModAssignFeedbackDelegate.instance.registerHandler(AddonModAssignFeedbackFileHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModAssignFeedbackFileComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    entryComponents: [
 | 
			
		||||
        AddonModAssignFeedbackFileComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignFeedbackFileModule {}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/feedback/file/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/feedback/file/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
    "pluginname": "File feedback"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								src/addons/mod/assign/feedback/file/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/addons/mod/assign/feedback/file/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,73 @@
 | 
			
		||||
// (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 {
 | 
			
		||||
    AddonModAssignPlugin,
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
} from '@addons/mod/assign/services/assign';
 | 
			
		||||
import { AddonModAssignFeedbackHandler } from '@addons/mod/assign/services/feedback-delegate';
 | 
			
		||||
import { Injectable, Type } from '@angular/core';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModAssignFeedbackFileComponent } from '../component/file';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler for file feedback plugin.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable( { providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignFeedbackFileHandlerService implements AddonModAssignFeedbackHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssignFeedbackFileHandler';
 | 
			
		||||
    type = 'file';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the Component to use to display the plugin data.
 | 
			
		||||
     * 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 AddonModAssignFeedbackFileComponent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignFeedbackFileHandler = makeSingleton(AddonModAssignFeedbackFileHandlerService);
 | 
			
		||||
							
								
								
									
										104
									
								
								src/addons/mod/assign/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/addons/mod/assign/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
			
		||||
{
 | 
			
		||||
    "acceptsubmissionstatement": "Please accept the submission statement.",
 | 
			
		||||
    "addattempt": "Allow another attempt",
 | 
			
		||||
    "addnewattempt": "Add a new attempt",
 | 
			
		||||
    "addnewattemptfromprevious": "Add a new attempt based on previous submission",
 | 
			
		||||
    "addsubmission": "Add submission",
 | 
			
		||||
    "allowsubmissionsfromdate": "Allow submissions from",
 | 
			
		||||
    "allowsubmissionsfromdatesummary": "This assignment will accept submissions from <strong>{{$a}}</strong>",
 | 
			
		||||
    "allowsubmissionsanddescriptionfromdatesummary": "The assignment details and submission form will be available from <strong>{{$a}}</strong>",
 | 
			
		||||
    "applytoteam": "Apply grades and feedback to entire group",
 | 
			
		||||
    "assignmentisdue": "Assignment is due",
 | 
			
		||||
    "attemptnumber": "Attempt number",
 | 
			
		||||
    "attemptreopenmethod": "Attempts reopened",
 | 
			
		||||
    "attemptreopenmethod_manual": "Manually",
 | 
			
		||||
    "attemptreopenmethod_untilpass": "Automatically until pass",
 | 
			
		||||
    "attemptsettings": "Attempt settings",
 | 
			
		||||
    "cannotgradefromapp": "Certain grading methods are not yet supported by the app and cannot be modified.",
 | 
			
		||||
    "cannoteditduetostatementsubmission": "You can't add or edit a submission in the app because the submission statement could not be retrieved from the site.",
 | 
			
		||||
    "cannotsubmitduetostatementsubmission": "You can't make a submission in the app because the submission statement could not be retrieved from the site.",
 | 
			
		||||
    "confirmsubmission": "Are you sure you want to submit your work for grading? You will not be able to make any more changes.",
 | 
			
		||||
    "currentgrade": "Current grade in gradebook",
 | 
			
		||||
    "cutoffdate": "Cut-off date",
 | 
			
		||||
    "currentattempt": "This is attempt {{$a}}.",
 | 
			
		||||
    "currentattemptof": "This is attempt {{$a.attemptnumber}} ( {{$a.maxattempts}} attempts allowed ).",
 | 
			
		||||
    "defaultteam": "Default group",
 | 
			
		||||
    "duedate": "Due date",
 | 
			
		||||
    "duedateno": "No due date",
 | 
			
		||||
    "duedatereached": "The due date for this assignment has now passed",
 | 
			
		||||
    "editingstatus": "Editing status",
 | 
			
		||||
    "editsubmission": "Edit submission",
 | 
			
		||||
    "erroreditpluginsnotsupported": "You can't add or edit a submission in the app because certain plugins are not yet supported for editing.",
 | 
			
		||||
    "errorshowinginformation": "Submission information cannot be displayed.",
 | 
			
		||||
    "extensionduedate": "Extension due date",
 | 
			
		||||
    "feedbacknotsupported": "This feedback is not supported by the app and may not contain all the information.",
 | 
			
		||||
    "grade": "Grade",
 | 
			
		||||
    "graded": "Graded",
 | 
			
		||||
    "gradedby": "Graded by",
 | 
			
		||||
    "gradedfollowupsubmit": "Graded - follow up submission received",
 | 
			
		||||
    "gradenotsynced": "Grade not synced",
 | 
			
		||||
    "gradedon": "Graded on",
 | 
			
		||||
    "gradelocked": "This grade is locked or overridden in the gradebook.",
 | 
			
		||||
    "gradeoutof": "Grade out of {{$a}}",
 | 
			
		||||
    "gradingstatus": "Grading status",
 | 
			
		||||
    "groupsubmissionsettings": "Group submission settings",
 | 
			
		||||
    "hiddenuser": "Participant",
 | 
			
		||||
    "latesubmissions": "Late submissions",
 | 
			
		||||
    "latesubmissionsaccepted": "Allowed until {{$a}}",
 | 
			
		||||
    "markingworkflowstate": "Marking workflow state",
 | 
			
		||||
    "markingworkflowstateinmarking": "In marking",
 | 
			
		||||
    "markingworkflowstateinreview": "In review",
 | 
			
		||||
    "markingworkflowstatenotmarked": "Not marked",
 | 
			
		||||
    "markingworkflowstatereadyforreview": "Marking completed",
 | 
			
		||||
    "markingworkflowstatereadyforrelease": "Ready for release",
 | 
			
		||||
    "markingworkflowstatereleased": "Released",
 | 
			
		||||
    "modulenameplural": "Assignments",
 | 
			
		||||
    "multipleteams": "Member of more than one group",
 | 
			
		||||
    "multipleteams_desc": "The assignment requires submission in groups. You are a member of more than one group. To be able to submit you must be a member of only one group. Please contact your teacher to change your group membership.",
 | 
			
		||||
    "noattempt": "No attempt",
 | 
			
		||||
    "nomoresubmissionsaccepted": "Only allowed for participants who have been granted an extension",
 | 
			
		||||
    "noonlinesubmissions": "This assignment does not require you to submit anything online",
 | 
			
		||||
    "nosubmission": "Nothing has been submitted for this assignment",
 | 
			
		||||
    "notallparticipantsareshown": "Participants who have not made a submission are not shown.",
 | 
			
		||||
    "noteam": "Not a member of any group",
 | 
			
		||||
    "noteam_desc": "This assignment requires submission in groups. You are not a member of any group, so you cannot create a submission. Please contact your teacher to be added to a group.",
 | 
			
		||||
    "notgraded": "Not graded",
 | 
			
		||||
    "numberofdraftsubmissions": "Drafts",
 | 
			
		||||
    "numberofparticipants": "Participants",
 | 
			
		||||
    "numberofsubmittedassignments": "Submitted",
 | 
			
		||||
    "numberofsubmissionsneedgrading": "Needs grading",
 | 
			
		||||
    "numberofteams": "Groups",
 | 
			
		||||
    "numwords": "{{$a}} words",
 | 
			
		||||
    "outof": "{{$a.current}} out of {{$a.total}}",
 | 
			
		||||
    "overdue": "<font color=\"red\">Assignment is overdue by: {{$a}}</font>",
 | 
			
		||||
    "submissioneditable": "Student can edit this submission",
 | 
			
		||||
    "submissionnoteditable": "Student cannot edit this submission",
 | 
			
		||||
    "submissionnotsupported": "This submission is not supported by the app and may not contain all the information.",
 | 
			
		||||
    "submission": "Submission",
 | 
			
		||||
    "submissionslocked": "This assignment is not accepting submissions",
 | 
			
		||||
    "submissionstatus_draft": "Draft (not submitted)",
 | 
			
		||||
    "submissionstatusheading": "Submission status",
 | 
			
		||||
    "submissionstatus_marked": "Graded",
 | 
			
		||||
    "submissionstatus_new": "No submission",
 | 
			
		||||
    "submissionstatus_reopened": "Reopened",
 | 
			
		||||
    "submissionstatus_submitted": "Submitted for grading",
 | 
			
		||||
    "submissionstatus_": "No submission",
 | 
			
		||||
    "submissionstatus": "Submission status",
 | 
			
		||||
    "submissionteam": "Group",
 | 
			
		||||
    "submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.",
 | 
			
		||||
    "submitassignment": "Submit assignment",
 | 
			
		||||
    "submittedearly": "Assignment was submitted {{$a}} early",
 | 
			
		||||
    "submittedlate": "Assignment was submitted {{$a}} late",
 | 
			
		||||
    "syncblockedusercomponent": "user grade",
 | 
			
		||||
    "timemodified": "Last modified",
 | 
			
		||||
    "timeremaining": "Time remaining",
 | 
			
		||||
    "ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.",
 | 
			
		||||
    "ungroupedusersoptional": "The setting 'Students submit in groups' is enabled and some users are either not a member of any group, or are a member of more than one group. Please be aware that these students will submit as members of the 'Default group'.",
 | 
			
		||||
    "unlimitedattempts": "Unlimited",
 | 
			
		||||
    "userwithid": "User with ID {{id}}",
 | 
			
		||||
    "userswhoneedtosubmit": "Users who need to submit: {{$a}}",
 | 
			
		||||
    "viewsubmission": "View submission",
 | 
			
		||||
    "warningsubmissionmodified": "The user submission was modified on the site.",
 | 
			
		||||
    "warningsubmissiongrademodified": "The submission grade was modified on the site.",
 | 
			
		||||
    "wordlimit": "Word limit"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								src/addons/mod/assign/pages/edit/edit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/addons/mod/assign/pages/edit/edit.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button fill="clear" (click)="save()" [attr.aria-label]="'core.save' | translate">
 | 
			
		||||
                {{ 'core.save' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <ion-list *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length">
 | 
			
		||||
            <!-- @todo: plagiarism_print_disclosure -->
 | 
			
		||||
            <form name="addon-mod_assign-edit-form" #editSubmissionForm>
 | 
			
		||||
                <!-- Submission statement. -->
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="submissionStatement">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <core-format-text [text]="submissionStatement" [filter]="false">
 | 
			
		||||
                        </core-format-text>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="submissionStatementAccepted"></ion-checkbox>
 | 
			
		||||
                    <!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. -->
 | 
			
		||||
                    <input item-content type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement">
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <addon-mod-assign-submission-plugin *ngFor="let plugin of userSubmission.plugins" [assign]="assign"
 | 
			
		||||
                    [submission]="userSubmission" [plugin]="plugin" [edit]="true" [allowOffline]="allowOffline">
 | 
			
		||||
                </addon-mod-assign-submission-plugin>
 | 
			
		||||
            </form>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										396
									
								
								src/addons/mod/assign/pages/edit/edit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										396
									
								
								src/addons/mod/assign/pages/edit/edit.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,396 @@
 | 
			
		||||
// (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, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreEventActivityDataSentData, CoreEvents } from '@singletons/events';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssignProvider,
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
    AddonModAssignSubmissionStatusOptions,
 | 
			
		||||
    AddonModAssignGetSubmissionStatusWSResponse,
 | 
			
		||||
    AddonModAssignSavePluginData,
 | 
			
		||||
    AddonModAssignSubmissionSavedEventData,
 | 
			
		||||
    AddonModAssignSubmittedForGradingEventData,
 | 
			
		||||
} from '../../services/assign';
 | 
			
		||||
import { AddonModAssignHelper } from '../../services/assign-helper';
 | 
			
		||||
import { AddonModAssignOffline } from '../../services/assign-offline';
 | 
			
		||||
import { AddonModAssignSync } from '../../services/assign-sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that allows adding or editing an assigment submission.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-assign-edit',
 | 
			
		||||
    templateUrl: 'edit.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignEditPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild('editSubmissionForm') formElement?: ElementRef;
 | 
			
		||||
 | 
			
		||||
    title: string; // Title to display.
 | 
			
		||||
    assign?: AddonModAssignAssign; // Assignment.
 | 
			
		||||
    courseId!: number; // Course ID the assignment belongs to.
 | 
			
		||||
    moduleId!: number; // Module ID the submission belongs to.
 | 
			
		||||
    userSubmission?: AddonModAssignSubmission; // The user submission.
 | 
			
		||||
    allowOffline = false; // Whether offline is allowed.
 | 
			
		||||
    submissionStatement?: string; // The submission statement.
 | 
			
		||||
    submissionStatementAccepted = false; // Whether submission statement is accepted.
 | 
			
		||||
    loaded = false; // Whether data has been loaded.
 | 
			
		||||
 | 
			
		||||
    protected userId: number; // User doing the submission.
 | 
			
		||||
    protected isBlind = false; // Whether blind is used.
 | 
			
		||||
    protected editText: string; // "Edit submission" translated text.
 | 
			
		||||
    protected saveOffline = false; // Whether to save data in offline.
 | 
			
		||||
    protected hasOffline = false; // Whether the assignment has offline data.
 | 
			
		||||
    protected isDestroyed = false; // Whether the component has been destroyed.
 | 
			
		||||
    protected forceLeave = false; // To allow leaving the page without checking for changes.
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.userId = CoreSites.instance.getCurrentSiteUserId(); // Right now we can only edit current user's submissions.
 | 
			
		||||
        this.editText = Translate.instance.instant('addon.mod_assign.editsubmission');
 | 
			
		||||
        this.title = this.editText;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!;
 | 
			
		||||
        this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
 | 
			
		||||
        this.isBlind = !!CoreNavigator.instance.getRouteNumberParam('blindId');
 | 
			
		||||
 | 
			
		||||
        this.fetchAssignment().finally(() => {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if we can leave the page or not.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Resolved if we can leave it, rejected if not.
 | 
			
		||||
     */
 | 
			
		||||
    async ionViewCanLeave(): Promise<void> {
 | 
			
		||||
        if (this.forceLeave) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if data has changed.
 | 
			
		||||
        const changed = await this.hasDataChanged();
 | 
			
		||||
        if (changed) {
 | 
			
		||||
            await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
 | 
			
		||||
        AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, this.getInputData());
 | 
			
		||||
 | 
			
		||||
        CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch assignment data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchAssignment(): Promise<void> {
 | 
			
		||||
        const currentUserId = CoreSites.instance.getCurrentSiteUserId();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Get assignment data.
 | 
			
		||||
            this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId);
 | 
			
		||||
            this.title = this.assign.name || this.title;
 | 
			
		||||
 | 
			
		||||
            if (!this.isDestroyed) {
 | 
			
		||||
                // Block the assignment.
 | 
			
		||||
                CoreSync.instance.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Wait for sync to be over (if any).
 | 
			
		||||
            await AddonModAssignSync.instance.waitForSync(this.assign.id);
 | 
			
		||||
 | 
			
		||||
            // Get submission status. Ignore cache to get the latest data.
 | 
			
		||||
            const options: AddonModAssignSubmissionStatusOptions = {
 | 
			
		||||
                userId: this.userId,
 | 
			
		||||
                isBlind: this.isBlind,
 | 
			
		||||
                cmId: this.assign.cmid,
 | 
			
		||||
                filter: false,
 | 
			
		||||
                readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let submissionStatus: AddonModAssignGetSubmissionStatusWSResponse;
 | 
			
		||||
            try {
 | 
			
		||||
                submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options);
 | 
			
		||||
                this.userSubmission =
 | 
			
		||||
                    AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                // Cannot connect. Get cached data.
 | 
			
		||||
                options.filter = true;
 | 
			
		||||
                options.readingStrategy = CoreSitesReadingStrategy.PreferCache;
 | 
			
		||||
 | 
			
		||||
                submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options);
 | 
			
		||||
                this.userSubmission =
 | 
			
		||||
                    AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt);
 | 
			
		||||
 | 
			
		||||
                // Check if the user can edit it in offline.
 | 
			
		||||
                const canEditOffline =
 | 
			
		||||
                    await AddonModAssignHelper.instance.canEditSubmissionOffline(this.assign, this.userSubmission);
 | 
			
		||||
                if (!canEditOffline) {
 | 
			
		||||
                    // Submission cannot be edited in offline, reject.
 | 
			
		||||
                    this.allowOffline = false;
 | 
			
		||||
                    throw error;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!submissionStatus.lastattempt?.canedit) {
 | 
			
		||||
                // Can't edit. Reject.
 | 
			
		||||
                throw new CoreError(Translate.instance.instant('core.nopermissions', { $a: this.editText }));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point.
 | 
			
		||||
            // Only show submission statement if we are editing our own submission.
 | 
			
		||||
            if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) {
 | 
			
		||||
                this.submissionStatement = this.assign.submissionstatement;
 | 
			
		||||
            } else {
 | 
			
		||||
                this.submissionStatement = undefined;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                // Check if there's any offline data for this submission.
 | 
			
		||||
                const offlineData = await AddonModAssignOffline.instance.getSubmission(this.assign.id, this.userId);
 | 
			
		||||
 | 
			
		||||
                this.hasOffline = offlineData?.plugindata && Object.keys(offlineData.plugindata).length > 0;
 | 
			
		||||
            } catch {
 | 
			
		||||
                // No offline data found.
 | 
			
		||||
                this.hasOffline = false;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting assigment data.');
 | 
			
		||||
 | 
			
		||||
            // Leave the player.
 | 
			
		||||
            this.leaveWithoutCheck();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the input data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Input data.
 | 
			
		||||
     */
 | 
			
		||||
    protected getInputData(): Record<string, unknown> {
 | 
			
		||||
        return CoreDomUtils.instance.getDataFromForm(document.forms['addon-mod_assign-edit-form']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if data has changed.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved with boolean: whether data has changed.
 | 
			
		||||
     */
 | 
			
		||||
    protected async hasDataChanged(): Promise<boolean> {
 | 
			
		||||
        // Usually the hasSubmissionDataChanged call will be resolved inmediately, causing the modal to be shown just an instant.
 | 
			
		||||
        // We'll wait a bit before showing it to prevent this "blink".
 | 
			
		||||
        const modal = await CoreDomUtils.instance.showModalLoading();
 | 
			
		||||
 | 
			
		||||
        const data = this.getInputData();
 | 
			
		||||
 | 
			
		||||
        return AddonModAssignHelper.instance.hasSubmissionDataChanged(this.assign!, this.userSubmission, data).finally(() => {
 | 
			
		||||
            modal.dismiss();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Leave the view without checking for changes.
 | 
			
		||||
     */
 | 
			
		||||
    protected leaveWithoutCheck(): void {
 | 
			
		||||
        this.forceLeave = true;
 | 
			
		||||
        CoreNavigator.instance.back();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get data to submit based on the input data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param inputData The input data.
 | 
			
		||||
     * @return Promise resolved with the data to submit.
 | 
			
		||||
     */
 | 
			
		||||
    protected prepareSubmissionData(inputData: Record<string, unknown>): Promise<AddonModAssignSavePluginData> {
 | 
			
		||||
        // If there's offline data, always save it in offline.
 | 
			
		||||
        this.saveOffline = this.hasOffline;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            return AddonModAssignHelper.instance.prepareSubmissionPluginData(
 | 
			
		||||
                this.assign!,
 | 
			
		||||
                this.userSubmission,
 | 
			
		||||
                inputData,
 | 
			
		||||
                this.hasOffline,
 | 
			
		||||
            );
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (this.allowOffline && !this.saveOffline) {
 | 
			
		||||
                // Cannot submit in online, prepare for offline usage.
 | 
			
		||||
                this.saveOffline = true;
 | 
			
		||||
 | 
			
		||||
                return AddonModAssignHelper.instance.prepareSubmissionPluginData(
 | 
			
		||||
                    this.assign!,
 | 
			
		||||
                    this.userSubmission,
 | 
			
		||||
                    inputData,
 | 
			
		||||
                    true,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            throw error;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save the submission.
 | 
			
		||||
     */
 | 
			
		||||
    async save(): Promise<void> {
 | 
			
		||||
        // Check if data has changed.
 | 
			
		||||
        const changed = await this.hasDataChanged();
 | 
			
		||||
        if (!changed) {
 | 
			
		||||
            // Nothing to save, just go back.
 | 
			
		||||
            this.leaveWithoutCheck();
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            await this.saveSubmission();
 | 
			
		||||
            this.leaveWithoutCheck();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'Error saving submission.');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save the submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async saveSubmission(): Promise<void> {
 | 
			
		||||
        const inputData = this.getInputData();
 | 
			
		||||
 | 
			
		||||
        if (this.submissionStatement && (!inputData.submissionstatement || inputData.submissionstatement === 'false')) {
 | 
			
		||||
            throw Translate.instance.instant('addon.mod_assign.acceptsubmissionstatement');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let modal = await CoreDomUtils.instance.showModalLoading();
 | 
			
		||||
        let size = -1;
 | 
			
		||||
 | 
			
		||||
        // Get size to ask for confirmation.
 | 
			
		||||
        try {
 | 
			
		||||
            size = await AddonModAssignHelper.instance.getSubmissionSizeForEdit(this.assign!, this.userSubmission!, inputData);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Error calculating size, return -1.
 | 
			
		||||
            size = -1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        modal.dismiss();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Confirm action.
 | 
			
		||||
            await CoreFileUploaderHelper.instance.confirmUploadFile(size, true, this.allowOffline);
 | 
			
		||||
 | 
			
		||||
            modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
 | 
			
		||||
 | 
			
		||||
            const pluginData = await this.prepareSubmissionData(inputData);
 | 
			
		||||
            if (!Object.keys(pluginData).length) {
 | 
			
		||||
                // Nothing to save.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let sent: boolean;
 | 
			
		||||
 | 
			
		||||
            if (this.saveOffline) {
 | 
			
		||||
                // Save submission in offline.
 | 
			
		||||
                sent = false;
 | 
			
		||||
                await AddonModAssignOffline.instance.saveSubmission(
 | 
			
		||||
                    this.assign!.id,
 | 
			
		||||
                    this.courseId,
 | 
			
		||||
                    pluginData,
 | 
			
		||||
                    this.userSubmission!.timemodified,
 | 
			
		||||
                    !this.assign!.submissiondrafts,
 | 
			
		||||
                    this.userId,
 | 
			
		||||
                );
 | 
			
		||||
            } else {
 | 
			
		||||
                // Try to send it to server.
 | 
			
		||||
                sent = await AddonModAssign.instance.saveSubmission(
 | 
			
		||||
                    this.assign!.id,
 | 
			
		||||
                    this.courseId,
 | 
			
		||||
                    pluginData,
 | 
			
		||||
                    this.allowOffline,
 | 
			
		||||
                    this.userSubmission!.timemodified,
 | 
			
		||||
                    !!this.assign!.submissiondrafts,
 | 
			
		||||
                    this.userId,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Clear temporary data from plugins.
 | 
			
		||||
            AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, inputData);
 | 
			
		||||
 | 
			
		||||
            if (sent) {
 | 
			
		||||
                CoreEvents.trigger<CoreEventActivityDataSentData>(CoreEvents.ACTIVITY_DATA_SENT, { module: 'assign' });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Submission saved, trigger events.
 | 
			
		||||
            CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.instance.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
            CoreEvents.trigger<AddonModAssignSubmissionSavedEventData>(
 | 
			
		||||
                AddonModAssignProvider.SUBMISSION_SAVED_EVENT,
 | 
			
		||||
                {
 | 
			
		||||
                    assignmentId: this.assign!.id,
 | 
			
		||||
                    submissionId: this.userSubmission!.id,
 | 
			
		||||
                    userId: this.userId,
 | 
			
		||||
                },
 | 
			
		||||
                CoreSites.instance.getCurrentSiteId(),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!this.assign!.submissiondrafts) {
 | 
			
		||||
                // No drafts allowed, so it was submitted. Trigger event.
 | 
			
		||||
                CoreEvents.trigger<AddonModAssignSubmittedForGradingEventData>(
 | 
			
		||||
                    AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT,
 | 
			
		||||
                    {
 | 
			
		||||
                        assignmentId: this.assign!.id,
 | 
			
		||||
                        submissionId: this.userSubmission!.id,
 | 
			
		||||
                        userId: this.userId,
 | 
			
		||||
                    },
 | 
			
		||||
                    CoreSites.instance.getCurrentSiteId(),
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            modal.dismiss();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.isDestroyed = true;
 | 
			
		||||
 | 
			
		||||
        // Unblock the assignment.
 | 
			
		||||
        if (this.assign) {
 | 
			
		||||
            CoreSync.instance.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/addons/mod/assign/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/addons/mod/assign/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <!-- The buttons defined by the component will be added in here. -->
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!assignComponent?.loaded" (ionRefresh)="assignComponent?.doRefresh($event)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
 | 
			
		||||
    <addon-mod-assign-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-assign-index>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										68
									
								
								src/addons/mod/assign/pages/index/index.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/addons/mod/assign/pages/index/index.page.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
// (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, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { CoreCourseWSModule } from '@features/course/services/course';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { AddonModAssignIndexComponent } from '../../components/index/index';
 | 
			
		||||
import { AddonModAssignAssign } from '../../services/assign';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays an assign.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-assign-index',
 | 
			
		||||
    templateUrl: 'index.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignIndexPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(AddonModAssignIndexComponent) assignComponent?: AddonModAssignIndexComponent;
 | 
			
		||||
 | 
			
		||||
    title?: string;
 | 
			
		||||
    module?: CoreCourseWSModule;
 | 
			
		||||
    courseId?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.module = CoreNavigator.instance.getRouteParam('module');
 | 
			
		||||
        this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
 | 
			
		||||
        this.title = this.module?.name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update some data based on the assign instance.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assign instance.
 | 
			
		||||
     */
 | 
			
		||||
    updateData(assign: AddonModAssignAssign): void {
 | 
			
		||||
        this.title = assign.name || this.title;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * User entered the page.
 | 
			
		||||
     */
 | 
			
		||||
    ionViewDidEnter(): void {
 | 
			
		||||
        this.assignComponent?.ionViewDidEnter();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * User left the page.
 | 
			
		||||
     */
 | 
			
		||||
    ionViewDidLeave(): void {
 | 
			
		||||
        this.assignComponent?.ionViewDidLeave();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,85 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId"  [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
 | 
			
		||||
        <ion-buttons slot="end"></ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-split-view>
 | 
			
		||||
        <ion-refresher slot="fixed" [disabled]="!loaded || !submissions.loaded" (ionRefresh)="refreshList($event)">
 | 
			
		||||
            <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
        </ion-refresher>
 | 
			
		||||
        <core-loading [hideUntil]="loaded && submissions.loaded">
 | 
			
		||||
            <core-empty-box *ngIf="!submissions || submissions.empty" icon="fas-file-signature"
 | 
			
		||||
                [message]="'addon.mod_assign.submissionstatus_' | translate">
 | 
			
		||||
            </core-empty-box>
 | 
			
		||||
 | 
			
		||||
            <ion-list>
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="(groupInfo.separateGroups || groupInfo.visibleGroups)">
 | 
			
		||||
                    <ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.separateGroups">
 | 
			
		||||
                        {{ 'core.groupsseparate' | translate }}
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.visibleGroups">
 | 
			
		||||
                        {{ 'core.groupsvisible' | translate }}
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-select [(ngModel)]="groupId" (ionChange)="setGroup(groupId)" aria-labelledby="addon-assign-groupslabel"
 | 
			
		||||
                        interface="action-sheet" slot="end">
 | 
			
		||||
                        <ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
 | 
			
		||||
                            {{groupOpt.name}}
 | 
			
		||||
                        </ion-select-option>
 | 
			
		||||
                    </ion-select>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <!-- List of submissions. -->
 | 
			
		||||
                <ng-container *ngFor="let submission of submissions.items">
 | 
			
		||||
                    <ion-item class="ion-text-wrap" (click)="submissions.select(submission)"
 | 
			
		||||
                        [class.core-selected-item]="submissions.isSelected(submission)">
 | 
			
		||||
                        <core-user-avatar [user]="submission" [linkProfile]="false" slot="start"></core-user-avatar>
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2 *ngIf="submission.userfullname">{{submission.userfullname}}</h2>
 | 
			
		||||
                            <h2 *ngIf="!submission.userfullname">
 | 
			
		||||
                                {{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}}
 | 
			
		||||
                            </h2>
 | 
			
		||||
                            <p *ngIf="assign && assign!.teamsubmission">
 | 
			
		||||
                                <span *ngIf="submission.groupname">{{submission.groupname}}</span>
 | 
			
		||||
                                <span *ngIf="assign!.preventsubmissionnotingroup && !submission.groupname && submission.noGroups
 | 
			
		||||
                                    && !submission.blindid" class="text-danger">
 | 
			
		||||
                                    {{ 'addon.mod_assign.noteam' | translate }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <span *ngIf="assign!.preventsubmissionnotingroup && !submission.groupname && submission.manyGroups
 | 
			
		||||
                                    && !submission.blindid" class="text-danger">
 | 
			
		||||
                                    {{ 'addon.mod_assign.multipleteams' | translate }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <span *ngIf="!assign!.preventsubmissionnotingroup && !submission.groupname">
 | 
			
		||||
                                    {{ 'addon.mod_assign.defaultteam' | translate }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <ion-badge class="ion-text-center ion-text-wrap" [color]="submission.statusColor"
 | 
			
		||||
                                *ngIf="submission.statusTranslated">
 | 
			
		||||
                                {{ submission.statusTranslated }}
 | 
			
		||||
                            </ion-badge>
 | 
			
		||||
                            <ion-badge class="ion-text-center ion-text-wrap" [color]="submission.gradingColor"
 | 
			
		||||
                                *ngIf="submission.gradingStatusTranslationId">
 | 
			
		||||
                                {{ submission.gradingStatusTranslationId | translate }}
 | 
			
		||||
                            </ion-badge>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
 | 
			
		||||
                <ion-card class="ion-text-wrap core-warning-card" *ngIf="!haveAllParticipants">
 | 
			
		||||
                    <ion-item>
 | 
			
		||||
                        <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
                        <ion-label>{{ 'addon.mod_assign.notallparticipantsareshown' | translate }}</ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ion-card>
 | 
			
		||||
            </ion-list>
 | 
			
		||||
        </core-loading>
 | 
			
		||||
    </core-split-view>
 | 
			
		||||
</ion-content>
 | 
			
		||||
@ -0,0 +1,393 @@
 | 
			
		||||
// (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, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router';
 | 
			
		||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
 | 
			
		||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreObject } from '@singletons/object';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssignProvider,
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
    AddonModAssignGradedEventData,
 | 
			
		||||
} from '../../services/assign';
 | 
			
		||||
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../../services/assign-helper';
 | 
			
		||||
import { AddonModAssignOffline } from '../../services/assign-offline';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignSyncProvider,
 | 
			
		||||
    AddonModAssignSync,
 | 
			
		||||
    AddonModAssignManualSyncData,
 | 
			
		||||
    AddonModAssignAutoSyncData,
 | 
			
		||||
} from '../../services/assign-sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays a list of submissions of an assignment.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-assign-submission-list',
 | 
			
		||||
    templateUrl: 'submission-list.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
 | 
			
		||||
 | 
			
		||||
    title = ''; // Title to display.
 | 
			
		||||
    assign?: AddonModAssignAssign; // Assignment.
 | 
			
		||||
    submissions: AddonModAssignSubmissionListManager; // List of submissions
 | 
			
		||||
    loaded = false; // Whether data has been loaded.
 | 
			
		||||
    haveAllParticipants  = true; // Whether all participants have been loaded.
 | 
			
		||||
    groupId = 0; // Group ID to show.
 | 
			
		||||
    courseId!: number; // Course ID the assignment belongs to.
 | 
			
		||||
    moduleId!: number; // Module ID the submission belongs to.
 | 
			
		||||
 | 
			
		||||
    groupInfo: CoreGroupInfo = {
 | 
			
		||||
        groups: [],
 | 
			
		||||
        separateGroups: false,
 | 
			
		||||
        visibleGroups: false,
 | 
			
		||||
        defaultGroupId: 0,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    protected selectedStatus?: string; // The status to see.
 | 
			
		||||
    protected gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes.
 | 
			
		||||
    protected syncObserver: CoreEventObserver; // Observer to refresh data when the async is synchronized.
 | 
			
		||||
    protected submissionsData: { canviewsubmissions: boolean; submissions?: AddonModAssignSubmission[] } = {
 | 
			
		||||
        canviewsubmissions: false,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.submissions = new AddonModAssignSubmissionListManager(AddonModAssignSubmissionListPage);
 | 
			
		||||
 | 
			
		||||
        // Update data if some grade changes.
 | 
			
		||||
        this.gradedObserver = CoreEvents.on<AddonModAssignGradedEventData>(
 | 
			
		||||
            AddonModAssignProvider.GRADED_EVENT,
 | 
			
		||||
            (data) => {
 | 
			
		||||
                if (
 | 
			
		||||
                    this.loaded &&
 | 
			
		||||
                    this.assign &&
 | 
			
		||||
                    data.assignmentId == this.assign.id &&
 | 
			
		||||
                    data.userId == CoreSites.instance.getCurrentSiteUserId()
 | 
			
		||||
                ) {
 | 
			
		||||
                    // Grade changed, refresh the data.
 | 
			
		||||
                    this.loaded = false;
 | 
			
		||||
 | 
			
		||||
                    this.refreshAllData(true).finally(() => {
 | 
			
		||||
                        this.loaded = true;
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            CoreSites.instance.getCurrentSiteId(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Refresh data if this assign is synchronized.
 | 
			
		||||
        const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED];
 | 
			
		||||
        this.syncObserver = CoreEvents.onMultiple<AddonModAssignAutoSyncData | AddonModAssignManualSyncData>(
 | 
			
		||||
            events,
 | 
			
		||||
            (data) => {
 | 
			
		||||
                if (!this.loaded || ('context' in data && data.context == 'submission-list')) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.loaded = false;
 | 
			
		||||
 | 
			
		||||
                this.refreshAllData(false).finally(() => {
 | 
			
		||||
                    this.loaded = true;
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            CoreSites.instance.getCurrentSiteId(),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    ngAfterViewInit(): void {
 | 
			
		||||
        this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!;
 | 
			
		||||
        this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
 | 
			
		||||
        this.groupId = CoreNavigator.instance.getRouteNumberParam('groupId') || 0;
 | 
			
		||||
        this.selectedStatus = CoreNavigator.instance.getRouteParam('status');
 | 
			
		||||
 | 
			
		||||
        if (this.selectedStatus) {
 | 
			
		||||
            if (this.selectedStatus == AddonModAssignProvider.NEED_GRADING) {
 | 
			
		||||
                this.title = Translate.instance.instant('addon.mod_assign.numberofsubmissionsneedgrading');
 | 
			
		||||
            } else {
 | 
			
		||||
                this.title = Translate.instance.instant('addon.mod_assign.submissionstatus_' + this.selectedStatus);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            this.title = Translate.instance.instant('addon.mod_assign.numberofparticipants');
 | 
			
		||||
        }
 | 
			
		||||
        this.fetchAssignment(true).finally(() => {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
            this.submissions.start(this.splitView);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch assignment data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sync Whether to try to synchronize data.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchAssignment(sync = false): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            // Get assignment data.
 | 
			
		||||
            this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId);
 | 
			
		||||
 | 
			
		||||
            this.title = this.assign.name || this.title;
 | 
			
		||||
 | 
			
		||||
            if (sync) {
 | 
			
		||||
                try {
 | 
			
		||||
                    // Try to synchronize data.
 | 
			
		||||
                    const result = await AddonModAssignSync.instance.syncAssign(this.assign.id);
 | 
			
		||||
 | 
			
		||||
                    if (result && result.updated) {
 | 
			
		||||
                        CoreEvents.trigger<AddonModAssignManualSyncData>(
 | 
			
		||||
                            AddonModAssignSyncProvider.MANUAL_SYNCED,
 | 
			
		||||
                            {
 | 
			
		||||
                                assignId: this.assign.id,
 | 
			
		||||
                                warnings: result.warnings,
 | 
			
		||||
                                gradesBlocked: result.gradesBlocked,
 | 
			
		||||
                                context: 'submission-list',
 | 
			
		||||
                            },
 | 
			
		||||
                            CoreSites.instance.getCurrentSiteId(),
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    // Ignore errors, probably user is offline or sync is blocked.
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get assignment submissions.
 | 
			
		||||
            this.submissionsData = await AddonModAssign.instance.getSubmissions(this.assign.id, { cmId: this.assign.cmid });
 | 
			
		||||
 | 
			
		||||
            if (!this.submissionsData.canviewsubmissions) {
 | 
			
		||||
                // User shouldn't be able to reach here.
 | 
			
		||||
                throw new Error('Cannot view submissions.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if groupmode is enabled to avoid showing wrong numbers.
 | 
			
		||||
            this.groupInfo = await CoreGroups.instance.getActivityGroupInfo(this.assign.cmid, false);
 | 
			
		||||
 | 
			
		||||
            await this.setGroup(CoreGroups.instance.validateGroupId(this.groupId, this.groupInfo));
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting assigment data.');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set group to see the summary.
 | 
			
		||||
     *
 | 
			
		||||
     * @param groupId Group ID.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async setGroup(groupId: number): Promise<void> {
 | 
			
		||||
        this.groupId = groupId;
 | 
			
		||||
 | 
			
		||||
        this.haveAllParticipants = true;
 | 
			
		||||
 | 
			
		||||
        if (!CoreSites.instance.getCurrentSite()?.wsAvailable('mod_assign_list_participants')) {
 | 
			
		||||
            // Submissions are not displayed in Moodle 3.1 without the local plugin, see MOBILE-2968.
 | 
			
		||||
            this.haveAllParticipants = false;
 | 
			
		||||
            this.submissions.resetItems();
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fetch submissions and grades.
 | 
			
		||||
        const submissions =
 | 
			
		||||
            await AddonModAssignHelper.instance.getSubmissionsUserData(
 | 
			
		||||
                this.assign!,
 | 
			
		||||
                this.submissionsData.submissions,
 | 
			
		||||
                this.groupId,
 | 
			
		||||
            );
 | 
			
		||||
        // Get assignment grades only if workflow is not enabled to check grading date.
 | 
			
		||||
        const grades = !this.assign!.markingworkflow
 | 
			
		||||
            ? await AddonModAssign.instance.getAssignmentGrades(this.assign!.id, { cmId: this.assign!.cmid })
 | 
			
		||||
            : [];
 | 
			
		||||
 | 
			
		||||
        // Filter the submissions to get only the ones with the right status and add some extra data.
 | 
			
		||||
        const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING;
 | 
			
		||||
        const searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus;
 | 
			
		||||
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
        const showSubmissions: AddonModAssignSubmissionForList[] = [];
 | 
			
		||||
 | 
			
		||||
        submissions.forEach((submission: AddonModAssignSubmissionForList) => {
 | 
			
		||||
            if (!searchStatus || searchStatus == submission.status) {
 | 
			
		||||
                promises.push(
 | 
			
		||||
                    CoreUtils.instance.ignoreErrors(
 | 
			
		||||
                        AddonModAssignOffline.instance.getSubmissionGrade(this.assign!.id, submission.userid),
 | 
			
		||||
                    ).then(async (data) => {
 | 
			
		||||
                        if (getNeedGrading) {
 | 
			
		||||
                            // Only show the submissions that need to be graded.
 | 
			
		||||
                            const add = await AddonModAssign.instance.needsSubmissionToBeGraded(submission, this.assign!.id);
 | 
			
		||||
 | 
			
		||||
                            if (!add) {
 | 
			
		||||
                                return;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Load offline grades.
 | 
			
		||||
                        const notSynced = !!data && submission.timemodified < data.timemodified;
 | 
			
		||||
 | 
			
		||||
                        if (submission.gradingstatus == 'graded' && !this.assign!.markingworkflow) {
 | 
			
		||||
                            // Get the last grade of the submission.
 | 
			
		||||
                            const grade = grades
 | 
			
		||||
                                .filter((grade) => grade.userid == submission.userid)
 | 
			
		||||
                                .reduce((a, b) => (a.timemodified > b.timemodified ? a : b));
 | 
			
		||||
 | 
			
		||||
                            if (grade && grade.timemodified < submission.timemodified) {
 | 
			
		||||
                                submission.gradingstatus = AddonModAssignProvider.GRADED_FOLLOWUP_SUBMIT;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        submission.statusColor = AddonModAssign.instance.getSubmissionStatusColor(submission.status);
 | 
			
		||||
                        submission.gradingColor = AddonModAssign.instance.getSubmissionGradingStatusColor(
 | 
			
		||||
                            submission.gradingstatus,
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                        // Show submission status if not submitted for grading.
 | 
			
		||||
                        if (submission.statusColor != 'success' || !submission.gradingstatus) {
 | 
			
		||||
                            submission.statusTranslated = Translate.instance.instant(
 | 
			
		||||
                                'addon.mod_assign.submissionstatus_' + submission.status,
 | 
			
		||||
                            );
 | 
			
		||||
                        } else {
 | 
			
		||||
                            submission.statusTranslated = '';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (notSynced) {
 | 
			
		||||
                            submission.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
 | 
			
		||||
                            submission.gradingColor = '';
 | 
			
		||||
                        } else if (submission.statusColor != 'danger' || submission.gradingColor != 'danger') {
 | 
			
		||||
                            // Show grading status if one of the statuses is not done.
 | 
			
		||||
                            submission.gradingStatusTranslationId = AddonModAssign.instance.getSubmissionGradingStatusTranslationId(
 | 
			
		||||
                                submission.gradingstatus,
 | 
			
		||||
                            );
 | 
			
		||||
                        } else {
 | 
			
		||||
                            submission.gradingStatusTranslationId = '';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        showSubmissions.push(submission);
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
                    }),
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        this.submissions.setItems(showSubmissions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh all the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sync Whether to try to synchronize data.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async refreshAllData(sync?: boolean): Promise<void> {
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId));
 | 
			
		||||
        if (this.assign) {
 | 
			
		||||
            promises.push(AddonModAssign.instance.invalidateAllSubmissionData(this.assign.id));
 | 
			
		||||
            promises.push(AddonModAssign.instance.invalidateAssignmentUserMappingsData(this.assign.id));
 | 
			
		||||
            promises.push(AddonModAssign.instance.invalidateAssignmentGradesData(this.assign.id));
 | 
			
		||||
            promises.push(AddonModAssign.instance.invalidateListParticipantsData(this.assign.id));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.fetchAssignment(sync);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the list.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     */
 | 
			
		||||
    refreshList(refresher?: CustomEvent<IonRefresher>): void {
 | 
			
		||||
        this.refreshAllData(true).finally(() => {
 | 
			
		||||
            refresher?.detail.complete();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.gradedObserver?.off();
 | 
			
		||||
        this.syncObserver?.off();
 | 
			
		||||
        this.submissions.destroy();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper class to manage submissions.
 | 
			
		||||
 */
 | 
			
		||||
class AddonModAssignSubmissionListManager extends CorePageItemsListManager<AddonModAssignSubmissionForList> {
 | 
			
		||||
 | 
			
		||||
    constructor(pageComponent: unknown) {
 | 
			
		||||
        super(pageComponent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected getItemPath(submission: AddonModAssignSubmissionForList): string {
 | 
			
		||||
        return String(submission.submitid);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected getItemQueryParams(submission: AddonModAssignSubmissionForList): Params {
 | 
			
		||||
        return CoreObject.withoutEmpty({
 | 
			
		||||
            blindId: submission.blindid,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null {
 | 
			
		||||
        return route.params.submitId ?? null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calculated data for an assign submission.
 | 
			
		||||
 */
 | 
			
		||||
type AddonModAssignSubmissionForList = AddonModAssignSubmissionFormatted & {
 | 
			
		||||
    statusColor?: string; // Calculated in the app. Color of the submission status.
 | 
			
		||||
    gradingColor?: string; // Calculated in the app. Color of the submission grading status.
 | 
			
		||||
    statusTranslated?: string; // Calculated in the app. Translated text of the submission status.
 | 
			
		||||
    gradingStatusTranslationId?: string; // Calculated in the app. Key of the text of the submission grading status.
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
 | 
			
		||||
        <ion-buttons slot="end"></ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
 | 
			
		||||
    <core-navbar-buttons slot="end">
 | 
			
		||||
        <ion-button [hidden]="!canSaveGrades" fill="clear" (click)="submitGrade()" [attr.aria-label]="'core.done' | translate">
 | 
			
		||||
            {{ 'core.done' | translate }}
 | 
			
		||||
        </ion-button>
 | 
			
		||||
    </core-navbar-buttons>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <addon-mod-assign-submission [courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId">
 | 
			
		||||
        </addon-mod-assign-submission>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
@ -0,0 +1,184 @@
 | 
			
		||||
// (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, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreScreen } from '@services/screen';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { AddonModAssignSubmissionComponent } from '../../components/submission/submission';
 | 
			
		||||
import { AddonModAssign, AddonModAssignAssign } from '../../services/assign';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays a submission.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-assign-submission-review',
 | 
			
		||||
    templateUrl: 'submission-review.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignSubmissionReviewPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
 | 
			
		||||
 | 
			
		||||
    title = ''; // Title to display.
 | 
			
		||||
    moduleId!: number; // Module ID the submission belongs to.
 | 
			
		||||
    courseId!: number; // Course ID the assignment belongs to.
 | 
			
		||||
    submitId!: number; //  User that did the submission.
 | 
			
		||||
    blindId?: number; // Blinded user ID (if it's blinded).
 | 
			
		||||
    loaded = false; // Whether data has been loaded.
 | 
			
		||||
    canSaveGrades = false; // Whether the user can save grades.
 | 
			
		||||
 | 
			
		||||
    protected assign?: AddonModAssignAssign; // The assignment the submission belongs to.
 | 
			
		||||
    protected blindMarking = false; // Whether it uses blind marking.
 | 
			
		||||
    protected forceLeave = false; // To allow leaving the page without checking for changes.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
    ) { }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.route.queryParams.subscribe((params) => {
 | 
			
		||||
            this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!;
 | 
			
		||||
            this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
 | 
			
		||||
            this.submitId = CoreNavigator.instance.getRouteNumberParam('submitId') || 0;
 | 
			
		||||
            this.blindId = CoreNavigator.instance.getRouteNumberParam('blindId', params);
 | 
			
		||||
 | 
			
		||||
            this.fetchSubmission().finally(() => {
 | 
			
		||||
                this.loaded = true;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if we can leave the page or not.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Resolved if we can leave it, rejected if not.
 | 
			
		||||
     */
 | 
			
		||||
    ionViewCanLeave(): boolean | Promise<void> {
 | 
			
		||||
        if (!this.submissionComponent || this.forceLeave) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if data has changed.
 | 
			
		||||
        return this.submissionComponent.canLeave();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * User entered the page.
 | 
			
		||||
     */
 | 
			
		||||
    ionViewDidEnter(): void {
 | 
			
		||||
        this.submissionComponent?.ionViewDidEnter();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * User left the page.
 | 
			
		||||
     */
 | 
			
		||||
    ionViewDidLeave(): void {
 | 
			
		||||
        this.submissionComponent?.ionViewDidLeave();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchSubmission(): Promise<void> {
 | 
			
		||||
        this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId);
 | 
			
		||||
        this.title = this.assign.name;
 | 
			
		||||
 | 
			
		||||
        this.blindMarking = !!this.assign.blindmarking && !this.assign.revealidentities;
 | 
			
		||||
 | 
			
		||||
        const gradeInfo = await CoreCourse.instance.getModuleBasicGradeInfo(this.moduleId);
 | 
			
		||||
        if (!gradeInfo) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Grades can be saved if simple grading.
 | 
			
		||||
        if (gradeInfo.advancedgrading && gradeInfo.advancedgrading[0] &&
 | 
			
		||||
                typeof gradeInfo.advancedgrading[0].method != 'undefined') {
 | 
			
		||||
 | 
			
		||||
            const method = gradeInfo.advancedgrading[0].method || 'simple';
 | 
			
		||||
            this.canSaveGrades = method == 'simple';
 | 
			
		||||
        } else {
 | 
			
		||||
            this.canSaveGrades = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh all the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async refreshAllData(): Promise<void> {
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId));
 | 
			
		||||
        if (this.assign) {
 | 
			
		||||
            promises.push(AddonModAssign.instance.invalidateSubmissionData(this.assign.id));
 | 
			
		||||
            promises.push(AddonModAssign.instance.invalidateAssignmentUserMappingsData(this.assign.id));
 | 
			
		||||
            promises.push(AddonModAssign.instance.invalidateSubmissionStatusData(
 | 
			
		||||
                this.assign.id,
 | 
			
		||||
                this.submitId,
 | 
			
		||||
                undefined,
 | 
			
		||||
                this.blindMarking,
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true);
 | 
			
		||||
 | 
			
		||||
            await this.fetchSubmission();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     */
 | 
			
		||||
    refreshSubmission(refresher?: CustomEvent<IonRefresher>): void {
 | 
			
		||||
        this.refreshAllData().finally(() => {
 | 
			
		||||
            refresher?.detail.complete();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Submit a grade and feedback.
 | 
			
		||||
     */
 | 
			
		||||
    async submitGrade(): Promise<void> {
 | 
			
		||||
        if (!this.submissionComponent) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.submissionComponent.submitGrade();
 | 
			
		||||
            // Grade submitted, leave the view if not in tablet.
 | 
			
		||||
            if (!CoreScreen.instance.isTablet) {
 | 
			
		||||
                this.forceLeave = true;
 | 
			
		||||
                CoreNavigator.instance.back();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										731
									
								
								src/addons/mod/assign/services/assign-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										731
									
								
								src/addons/mod/assign/services/assign-helper.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,731 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
 | 
			
		||||
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { FileEntry } from '@ionic-native/file/ngx';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignProvider,
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssignParticipant,
 | 
			
		||||
    AddonModAssignSubmissionFeedback,
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
    AddonModAssignPlugin,
 | 
			
		||||
    AddonModAssignSavePluginData,
 | 
			
		||||
} from './assign';
 | 
			
		||||
import { AddonModAssignOffline } from './assign-offline';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreFile } from '@services/file';
 | 
			
		||||
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
 | 
			
		||||
import { CoreGroups } from '@services/groups';
 | 
			
		||||
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
 | 
			
		||||
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service that provides some helper functions for assign.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignHelperProvider {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a submission can be edited in offline.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param submission Submission.
 | 
			
		||||
     * @return Whether it can be edited offline.
 | 
			
		||||
     */
 | 
			
		||||
    async canEditSubmissionOffline(assign: AddonModAssignAssign, submission?: AddonModAssignSubmission): Promise<boolean> {
 | 
			
		||||
        if (!submission) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (submission.status == AddonModAssignProvider.SUBMISSION_STATUS_NEW ||
 | 
			
		||||
                submission.status == AddonModAssignProvider.SUBMISSION_STATUS_REOPENED) {
 | 
			
		||||
            // It's a new submission, allow creating it in offline.
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let canEdit = true;
 | 
			
		||||
 | 
			
		||||
        const promises = submission.plugins
 | 
			
		||||
            ? submission.plugins.map((plugin) =>
 | 
			
		||||
                AddonModAssignSubmissionDelegate.instance.canPluginEditOffline(assign, submission, plugin).then((canEditPlugin) => {
 | 
			
		||||
                    if (!canEditPlugin) {
 | 
			
		||||
                        canEdit = false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }))
 | 
			
		||||
            : [];
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        return canEdit;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Clear plugins temporary data because a submission was cancelled.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param submission Submission to clear the data for.
 | 
			
		||||
     * @param inputData Data entered in the submission form.
 | 
			
		||||
     */
 | 
			
		||||
    clearSubmissionPluginTmpData(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission | undefined,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
    ): void {
 | 
			
		||||
        if (!submission) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        submission.plugins?.forEach((plugin) => {
 | 
			
		||||
            AddonModAssignSubmissionDelegate.instance.clearTmpData(assign, submission, plugin, inputData);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Copy the data from last submitted attempt to the current submission.
 | 
			
		||||
     * Since we don't have any WS for that we'll have to re-submit everything manually.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param previousSubmission Submission to copy.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async copyPreviousAttempt(assign: AddonModAssignAssign, previousSubmission: AddonModAssignSubmission): Promise<void> {
 | 
			
		||||
        const pluginData: AddonModAssignSavePluginData = {};
 | 
			
		||||
        const promises = previousSubmission.plugins
 | 
			
		||||
            ? previousSubmission.plugins.map((plugin) =>
 | 
			
		||||
                AddonModAssignSubmissionDelegate.instance.copyPluginSubmissionData(assign, plugin, pluginData))
 | 
			
		||||
            : [];
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        // We got the plugin data. Now we need to submit it.
 | 
			
		||||
        if (Object.keys(pluginData).length) {
 | 
			
		||||
            // There's something to save.
 | 
			
		||||
            return AddonModAssign.instance.saveSubmissionOnline(assign.id, pluginData);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create an empty feedback object.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Feedback.
 | 
			
		||||
     */
 | 
			
		||||
    createEmptyFeedback(): AddonModAssignSubmissionFeedback {
 | 
			
		||||
        return {
 | 
			
		||||
            grade: undefined,
 | 
			
		||||
            gradefordisplay: '',
 | 
			
		||||
            gradeddate: 0,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create an empty submission object.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Submission.
 | 
			
		||||
     */
 | 
			
		||||
    createEmptySubmission(): AddonModAssignSubmissionFormatted {
 | 
			
		||||
        return {
 | 
			
		||||
            id: 0,
 | 
			
		||||
            userid: 0,
 | 
			
		||||
            attemptnumber: 0,
 | 
			
		||||
            timecreated: 0,
 | 
			
		||||
            timemodified: 0,
 | 
			
		||||
            status: '',
 | 
			
		||||
            groupid: 0,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete stored submission files for a plugin. See storeSubmissionFiles.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async deleteStoredSubmissionFiles(assignId: number, folderName: string, userId?: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const folderPath = await AddonModAssignOffline.instance.getSubmissionPluginFolder(assignId, folderName, userId, siteId);
 | 
			
		||||
 | 
			
		||||
        await CoreFile.instance.removeDir(folderPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete all drafts of the feedback plugin data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment Id.
 | 
			
		||||
     * @param userId User Id.
 | 
			
		||||
     * @param feedback Feedback data.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async discardFeedbackPluginData(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        feedback: AddonModAssignSubmissionFeedback,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        const promises = feedback.plugins
 | 
			
		||||
            ? feedback.plugins.map((plugin) =>
 | 
			
		||||
                AddonModAssignFeedbackDelegate.instance.discardPluginFeedbackData(assignId, userId, plugin, siteId))
 | 
			
		||||
            : [];
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a submission has no content.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment object.
 | 
			
		||||
     * @param submission Submission to inspect.
 | 
			
		||||
     * @return Whether the submission is empty.
 | 
			
		||||
     */
 | 
			
		||||
    isSubmissionEmpty(assign: AddonModAssignAssign, submission?: AddonModAssignSubmission): boolean {
 | 
			
		||||
        if (!submission) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const anyNotEmpty = submission.plugins?.some((plugin) =>
 | 
			
		||||
            !AddonModAssignSubmissionDelegate.instance.isPluginEmpty(assign, plugin));
 | 
			
		||||
 | 
			
		||||
        // If any plugin is not empty, we consider that the submission is not empty either.
 | 
			
		||||
        if (anyNotEmpty) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // If all the plugins were empty (or there were no plugins), we consider the submission to be empty.
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * List the participants for a single assignment, with some summary info about their submissions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment object.
 | 
			
		||||
     * @param groupId Group Id.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the list of participants and summary of submissions.
 | 
			
		||||
     */
 | 
			
		||||
    async getParticipants(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        groupId?: number,
 | 
			
		||||
        options: CoreSitesCommonWSOptions = {},
 | 
			
		||||
    ): Promise<AddonModAssignParticipant[]> {
 | 
			
		||||
 | 
			
		||||
        groupId = groupId || 0;
 | 
			
		||||
        options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        // Create new options including all existing ones.
 | 
			
		||||
        const modOptions: CoreCourseCommonModWSOptions = { cmId: assign.cmid, ...options };
 | 
			
		||||
 | 
			
		||||
        const participants = await AddonModAssign.instance.listParticipants(assign.id, groupId, modOptions);
 | 
			
		||||
 | 
			
		||||
        if (groupId || participants && participants.length > 0) {
 | 
			
		||||
            return participants;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If no participants returned and all groups specified, get participants by groups.
 | 
			
		||||
        const groupsInfo = await CoreGroups.instance.getActivityGroupInfo(assign.cmid, false, undefined, modOptions.siteId);
 | 
			
		||||
 | 
			
		||||
        const participantsIndexed: {[id: number]: AddonModAssignParticipant} = {};
 | 
			
		||||
 | 
			
		||||
        const promises = groupsInfo.groups
 | 
			
		||||
            ? groupsInfo.groups.map((userGroup) =>
 | 
			
		||||
                AddonModAssign.instance.listParticipants(assign.id, userGroup.id, modOptions).then((participantsFromList) => {
 | 
			
		||||
                    // Do not get repeated users.
 | 
			
		||||
                    participantsFromList.forEach((participant) => {
 | 
			
		||||
                        participantsIndexed[participant.id] = participant;
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }))
 | 
			
		||||
            :[];
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        return CoreUtils.instance.objectToArray(participantsIndexed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get plugin config from assignment config.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment object including all config.
 | 
			
		||||
     * @param subtype Subtype name (assignsubmission or assignfeedback)
 | 
			
		||||
     * @param type Name of the subplugin.
 | 
			
		||||
     * @return Object containing all configurations of the subplugin selected.
 | 
			
		||||
     */
 | 
			
		||||
    getPluginConfig(assign: AddonModAssignAssign, subtype: string, type: string): AddonModAssignPluginConfig {
 | 
			
		||||
        const configs: AddonModAssignPluginConfig = {};
 | 
			
		||||
 | 
			
		||||
        assign.configs.forEach((config) => {
 | 
			
		||||
            if (config.subtype == subtype && config.plugin == type) {
 | 
			
		||||
                configs[config.name] = config.value;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return configs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get enabled subplugins.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment object including all config.
 | 
			
		||||
     * @param subtype Subtype name (assignsubmission or assignfeedback)
 | 
			
		||||
     * @return List of enabled plugins for the assign.
 | 
			
		||||
     */
 | 
			
		||||
    getPluginsEnabled(assign: AddonModAssignAssign, subtype: string): AddonModAssignPlugin[] {
 | 
			
		||||
        const enabled: AddonModAssignPlugin[] = [];
 | 
			
		||||
 | 
			
		||||
        assign.configs.forEach((config) => {
 | 
			
		||||
            if (config.subtype == subtype && config.name == 'enabled' && parseInt(config.value, 10) === 1) {
 | 
			
		||||
                // Format the plugin objects.
 | 
			
		||||
                enabled.push({
 | 
			
		||||
                    type: config.plugin,
 | 
			
		||||
                    name: config.plugin,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return enabled;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a list of stored submission files. See storeSubmissionFiles.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the files.
 | 
			
		||||
     */
 | 
			
		||||
    async getStoredSubmissionFiles(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        folderName: string,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<(FileEntry | DirectoryEntry)[]> {
 | 
			
		||||
        const folderPath = await AddonModAssignOffline.instance.getSubmissionPluginFolder(assignId, folderName, userId, siteId);
 | 
			
		||||
 | 
			
		||||
        return CoreFile.instance.getDirectoryContents(folderPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the size that will be uploaded to perform an attempt copy.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param previousSubmission Submission to copy.
 | 
			
		||||
     * @return Promise resolved with the size.
 | 
			
		||||
     */
 | 
			
		||||
    async getSubmissionSizeForCopy(assign: AddonModAssignAssign, previousSubmission: AddonModAssignSubmission): Promise<number> {
 | 
			
		||||
        let totalSize = 0;
 | 
			
		||||
 | 
			
		||||
        const promises = previousSubmission.plugins
 | 
			
		||||
            ? previousSubmission.plugins.map((plugin) =>
 | 
			
		||||
                AddonModAssignSubmissionDelegate.instance.getPluginSizeForCopy(assign, plugin).then((size) => {
 | 
			
		||||
                    totalSize += (size || 0);
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }))
 | 
			
		||||
            : [];
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        return totalSize;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the size that will be uploaded to save a submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param submission Submission to check data.
 | 
			
		||||
     * @param inputData Data entered in the submission form.
 | 
			
		||||
     * @return Promise resolved with the size.
 | 
			
		||||
     */
 | 
			
		||||
    async getSubmissionSizeForEdit(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
    ): Promise<number> {
 | 
			
		||||
 | 
			
		||||
        let totalSize = 0;
 | 
			
		||||
 | 
			
		||||
        const promises = submission.plugins
 | 
			
		||||
            ? submission.plugins.map((plugin) =>
 | 
			
		||||
                AddonModAssignSubmissionDelegate.instance.getPluginSizeForEdit(assign, submission, plugin, inputData)
 | 
			
		||||
                    .then((size) => {
 | 
			
		||||
                        totalSize += (size || 0);
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
                    }))
 | 
			
		||||
            : [];
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        return totalSize;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get user data for submissions since they only have userid.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment object.
 | 
			
		||||
     * @param submissions Submissions to get the data for.
 | 
			
		||||
     * @param groupId Group Id.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise always resolved. Resolve param is the formatted submissions.
 | 
			
		||||
     */
 | 
			
		||||
    async getSubmissionsUserData(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submissions: AddonModAssignSubmissionFormatted[] = [],
 | 
			
		||||
        groupId?: number,
 | 
			
		||||
        options: CoreSitesCommonWSOptions = {},
 | 
			
		||||
    ): Promise<AddonModAssignSubmissionFormatted[]> {
 | 
			
		||||
        // Create new options including all existing ones.
 | 
			
		||||
        const modOptions: CoreCourseCommonModWSOptions = { cmId: assign.cmid, ...options };
 | 
			
		||||
 | 
			
		||||
        const parts = await this.getParticipants(assign, groupId, options);
 | 
			
		||||
 | 
			
		||||
        const blind = assign.blindmarking && !assign.revealidentities;
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
        const result: AddonModAssignSubmissionFormatted[] = [];
 | 
			
		||||
        const participants: {[id: number]: AddonModAssignParticipant} = CoreUtils.instance.arrayToObject(parts, 'id');
 | 
			
		||||
 | 
			
		||||
        submissions.forEach((submission) => {
 | 
			
		||||
            submission.submitid = submission.userid && submission.userid > 0 ? submission.userid : submission.blindid;
 | 
			
		||||
            if (typeof submission.submitid == 'undefined' || submission.submitid <= 0) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const participant = participants[submission.submitid];
 | 
			
		||||
            if (!participant) {
 | 
			
		||||
                // Avoid permission denied error. Participant not found on list.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            delete participants[submission.submitid];
 | 
			
		||||
 | 
			
		||||
            if (!blind) {
 | 
			
		||||
                submission.userfullname = participant.fullname;
 | 
			
		||||
                submission.userprofileimageurl = participant.profileimageurl;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            submission.manyGroups = !!participant.groups && participant.groups.length > 1;
 | 
			
		||||
            submission.noGroups = !!participant.groups && participant.groups.length == 0;
 | 
			
		||||
            if (participant.groupname) {
 | 
			
		||||
                submission.groupid = participant.groupid!;
 | 
			
		||||
                submission.groupname = participant.groupname;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let promise = Promise.resolve();
 | 
			
		||||
            if (submission.userid && submission.userid > 0 && blind) {
 | 
			
		||||
                // Blind but not blinded! (Moodle < 3.1.1, 3.2).
 | 
			
		||||
                delete submission.userid;
 | 
			
		||||
 | 
			
		||||
                promise = AddonModAssign.instance.getAssignmentUserMappings(assign.id, submission.submitid, modOptions)
 | 
			
		||||
                    .then((blindId) => {
 | 
			
		||||
                        submission.blindid = blindId;
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
                    });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            promises.push(promise.then(() => {
 | 
			
		||||
                // Add to the list.
 | 
			
		||||
                if (submission.userfullname || submission.blindid) {
 | 
			
		||||
                    result.push(submission);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        // Create a submission for each participant left in the list (the participants already treated were removed).
 | 
			
		||||
        CoreUtils.instance.objectToArray(participants).forEach((participant: AddonModAssignParticipant) => {
 | 
			
		||||
            const submission = this.createEmptySubmission();
 | 
			
		||||
 | 
			
		||||
            submission.submitid = participant.id;
 | 
			
		||||
 | 
			
		||||
            if (!blind) {
 | 
			
		||||
                submission.userid = participant.id;
 | 
			
		||||
                submission.userfullname = participant.fullname;
 | 
			
		||||
                submission.userprofileimageurl = participant.profileimageurl;
 | 
			
		||||
            } else {
 | 
			
		||||
                submission.blindid = participant.id;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            submission.manyGroups = !!participant.groups && participant.groups.length > 1;
 | 
			
		||||
            submission.noGroups = !!participant.groups && participant.groups.length == 0;
 | 
			
		||||
            if (participant.groupname) {
 | 
			
		||||
                submission.groupid = participant.groupid!;
 | 
			
		||||
                submission.groupname = participant.groupname;
 | 
			
		||||
            }
 | 
			
		||||
            submission.status = participant.submitted ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED :
 | 
			
		||||
                AddonModAssignProvider.SUBMISSION_STATUS_NEW;
 | 
			
		||||
 | 
			
		||||
            result.push(submission);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the feedback data has changed for a certain submission and assign.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param submission The submission.
 | 
			
		||||
     * @param feedback Feedback data.
 | 
			
		||||
     * @param userId The user ID.
 | 
			
		||||
     * @return Promise resolved with true if data has changed, resolved with false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async hasFeedbackDataChanged(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted | undefined,
 | 
			
		||||
        feedback: AddonModAssignSubmissionFeedback,
 | 
			
		||||
        userId: number,
 | 
			
		||||
    ): Promise<boolean> {
 | 
			
		||||
        if (!submission || !feedback.plugins) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let hasChanged = false;
 | 
			
		||||
 | 
			
		||||
        const promises = feedback.plugins.map((plugin) =>
 | 
			
		||||
            this.prepareFeedbackPluginData(assign.id, userId, feedback).then(async (inputData) => {
 | 
			
		||||
                const changed = await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
                    AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData, userId),
 | 
			
		||||
                    false,
 | 
			
		||||
                );
 | 
			
		||||
                if (changed) {
 | 
			
		||||
                    hasChanged = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.allPromises(promises);
 | 
			
		||||
 | 
			
		||||
        return hasChanged;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the submission data has changed for a certain submission and assign.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param submission Submission to check data.
 | 
			
		||||
     * @param inputData Data entered in the submission form.
 | 
			
		||||
     * @return Promise resolved with true if data has changed, resolved with false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async hasSubmissionDataChanged(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission | undefined,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
    ): Promise<boolean> {
 | 
			
		||||
        if (!submission) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let hasChanged = false;
 | 
			
		||||
 | 
			
		||||
        const promises = submission.plugins
 | 
			
		||||
            ? submission.plugins.map((plugin) =>
 | 
			
		||||
                AddonModAssignSubmissionDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData)
 | 
			
		||||
                    .then((changed) => {
 | 
			
		||||
                        if (changed) {
 | 
			
		||||
                            hasChanged = true;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
                    }).catch(() => {
 | 
			
		||||
                        // Ignore errors.
 | 
			
		||||
                    }))
 | 
			
		||||
            : [];
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.allPromises(promises);
 | 
			
		||||
 | 
			
		||||
        return hasChanged;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and return the plugin data to send for a certain feedback and assign.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment Id.
 | 
			
		||||
     * @param userId User Id.
 | 
			
		||||
     * @param feedback Feedback data.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with plugin data to send to server.
 | 
			
		||||
     */
 | 
			
		||||
    async prepareFeedbackPluginData(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        feedback: AddonModAssignSubmissionFeedback,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModAssignSavePluginData> {
 | 
			
		||||
 | 
			
		||||
        const pluginData: Record<string, unknown> = {};
 | 
			
		||||
        const promises = feedback.plugins
 | 
			
		||||
            ? feedback.plugins.map((plugin) =>
 | 
			
		||||
                AddonModAssignFeedbackDelegate.instance.preparePluginFeedbackData(assignId, userId, plugin, pluginData, siteId))
 | 
			
		||||
            : [];
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        return pluginData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and return the plugin data to send for a certain submission and assign.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param submission Submission to check data.
 | 
			
		||||
     * @param inputData Data entered in the submission form.
 | 
			
		||||
     * @param offline True to prepare the data for an offline submission, false otherwise.
 | 
			
		||||
     * @return Promise resolved with plugin data to send to server.
 | 
			
		||||
     */
 | 
			
		||||
    async prepareSubmissionPluginData(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission | undefined,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
        offline = false,
 | 
			
		||||
    ): Promise<AddonModAssignSavePluginData> {
 | 
			
		||||
 | 
			
		||||
        if (!submission || !submission.plugins) {
 | 
			
		||||
            return {};
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const pluginData: AddonModAssignSavePluginData = {};
 | 
			
		||||
        const promises = submission.plugins.map((plugin) =>
 | 
			
		||||
            AddonModAssignSubmissionDelegate.instance.preparePluginSubmissionData(
 | 
			
		||||
                assign,
 | 
			
		||||
                submission,
 | 
			
		||||
                plugin,
 | 
			
		||||
                inputData,
 | 
			
		||||
                pluginData,
 | 
			
		||||
                offline,
 | 
			
		||||
            ));
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        return pluginData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a list of files (either online files or local files), store the local files in a local folder
 | 
			
		||||
     * to be submitted later.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
 | 
			
		||||
     * @param files List of files.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if success, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async storeSubmissionFiles(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        folderName: string,
 | 
			
		||||
        files: (CoreWSExternalFile | FileEntry)[],
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<CoreFileUploaderStoreFilesResult> {
 | 
			
		||||
        // Get the folder where to store the files.
 | 
			
		||||
        const folderPath = await AddonModAssignOffline.instance.getSubmissionPluginFolder(assignId, folderName, userId, siteId);
 | 
			
		||||
 | 
			
		||||
        return CoreFileUploader.instance.storeFilesToUpload(folderPath, files);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Upload a file to a draft area. If the file is an online file it will be downloaded and then re-uploaded.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param file Online file or local FileEntry.
 | 
			
		||||
     * @param itemId Draft ID to use. Undefined or 0 to create a new draft ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the itemId.
 | 
			
		||||
     */
 | 
			
		||||
    uploadFile(assignId: number, file: CoreWSExternalFile | FileEntry, itemId?: number, siteId?: string): Promise<number> {
 | 
			
		||||
        return CoreFileUploader.instance.uploadOrReuploadFile(file, itemId, AddonModAssignProvider.COMPONENT, assignId, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a list of files (either online files or local files), upload them to a draft area and return the draft ID.
 | 
			
		||||
     * Online files will be downloaded and then re-uploaded.
 | 
			
		||||
     * If there are no files to upload it will return a fake draft ID (1).
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param files List of files.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the itemId.
 | 
			
		||||
     */
 | 
			
		||||
    uploadFiles(assignId: number, files: (CoreWSExternalFile | FileEntry)[], siteId?: string): Promise<number> {
 | 
			
		||||
        return CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModAssignProvider.COMPONENT, assignId, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Upload or store some files, depending if the user is offline or not.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
 | 
			
		||||
     * @param files List of files.
 | 
			
		||||
     * @param offline True if files sould be stored for offline, false to upload them.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async uploadOrStoreFiles(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        folderName: string,
 | 
			
		||||
        files: (CoreWSExternalFile | FileEntry)[],
 | 
			
		||||
        offline = false,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<number | CoreFileUploaderStoreFilesResult> {
 | 
			
		||||
 | 
			
		||||
        if (offline) {
 | 
			
		||||
            return await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await this.uploadFiles(assignId, files, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignHelper = makeSingleton(AddonModAssignHelperProvider);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Assign submission with some calculated data.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModAssignSubmissionFormatted =
 | 
			
		||||
    Omit<AddonModAssignSubmission, 'userid'> & {
 | 
			
		||||
        userid?: number; // Student id.
 | 
			
		||||
        blindid?: number; // Calculated in the app. Blindid of the user that did the submission.
 | 
			
		||||
        submitid?: number; // Calculated in the app. Userid or blindid of the user that did the submission.
 | 
			
		||||
        userfullname?: string; // Calculated in the app. Full name of the user that did the submission.
 | 
			
		||||
        userprofileimageurl?: string; // Calculated in the app. Avatar of the user that did the submission.
 | 
			
		||||
        manyGroups?: boolean; // Calculated in the app. Whether the user belongs to more than 1 group.
 | 
			
		||||
        noGroups?: boolean; // Calculated in the app. Whether the user doesn't belong to any group.
 | 
			
		||||
        groupname?: string; // Calculated in the app. Name of the group the submission belongs to.
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Assignment plugin config.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModAssignPluginConfig = {[name: string]: string};
 | 
			
		||||
							
								
								
									
										459
									
								
								src/addons/mod/assign/services/assign-offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										459
									
								
								src/addons/mod/assign/services/assign-offline.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,459 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
import { CoreFile } from '@services/file';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModAssignOutcomes, AddonModAssignSavePluginData } from './assign';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignSubmissionsDBRecord,
 | 
			
		||||
    AddonModAssignSubmissionsGradingDBRecord,
 | 
			
		||||
    SUBMISSIONS_GRADES_TABLE,
 | 
			
		||||
    SUBMISSIONS_TABLE,
 | 
			
		||||
} from './database/assign';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to handle offline assign.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignOfflineProvider {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete a submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if deleted, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    async deleteSubmission(assignId: number, userId?: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
        userId = userId || site.getUserId();
 | 
			
		||||
 | 
			
		||||
        await site.getDb().deleteRecords(
 | 
			
		||||
            SUBMISSIONS_TABLE,
 | 
			
		||||
            { assignid: assignId, userid: userId },
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete a submission grade.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if deleted, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    async deleteSubmissionGrade(assignId: number, userId?: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
        userId = userId || site.getUserId();
 | 
			
		||||
 | 
			
		||||
        await site.getDb().deleteRecords(
 | 
			
		||||
            SUBMISSIONS_GRADES_TABLE,
 | 
			
		||||
            { assignid: assignId, userid: userId },
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the assignments ids that have something to be synced.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with assignments id that have something to be synced.
 | 
			
		||||
     */
 | 
			
		||||
    async getAllAssigns(siteId?: string): Promise<number[]> {
 | 
			
		||||
        const promises:
 | 
			
		||||
        Promise<AddonModAssignSubmissionsDBRecordFormatted[] | AddonModAssignSubmissionsGradingDBRecordFormatted[]>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(this.getAllSubmissions(siteId));
 | 
			
		||||
        promises.push(this.getAllSubmissionsGrade(siteId));
 | 
			
		||||
 | 
			
		||||
        const results = await Promise.all(promises);
 | 
			
		||||
        // Flatten array.
 | 
			
		||||
        const flatten: (AddonModAssignSubmissionsDBRecord | AddonModAssignSubmissionsGradingDBRecord)[] =
 | 
			
		||||
            [].concat.apply([], results);
 | 
			
		||||
 | 
			
		||||
        // Get assign id.
 | 
			
		||||
        let assignIds: number[] = flatten.map((assign) => assign.assignid);
 | 
			
		||||
        // Get unique values.
 | 
			
		||||
        assignIds = assignIds.filter((id, pos) => assignIds.indexOf(id) == pos);
 | 
			
		||||
 | 
			
		||||
        return assignIds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the stored submissions from all the assignments.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with submissions.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getAllSubmissions(siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> {
 | 
			
		||||
        return this.getAssignSubmissionsFormatted(undefined, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the stored submissions for a certain assignment.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with submissions.
 | 
			
		||||
     */
 | 
			
		||||
    async getAssignSubmissions(assignId: number, siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> {
 | 
			
		||||
        return this.getAssignSubmissionsFormatted({ assignid: assignId }, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convenience helper function to get stored submissions formatted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param conditions Query conditions.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with submissions.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getAssignSubmissionsFormatted(
 | 
			
		||||
        conditions: SQLiteDBRecordValues = {},
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> {
 | 
			
		||||
        const db = await CoreSites.instance.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        const submissions: AddonModAssignSubmissionsDBRecord[] = await db.getRecords(SUBMISSIONS_TABLE, conditions);
 | 
			
		||||
 | 
			
		||||
        // Parse the plugin data.
 | 
			
		||||
        return submissions.map((submission) => ({
 | 
			
		||||
            assignid: submission.assignid,
 | 
			
		||||
            userid: submission.userid,
 | 
			
		||||
            courseid: submission.courseid,
 | 
			
		||||
            plugindata: CoreTextUtils.instance.parseJSON<AddonModAssignSavePluginData>(submission.plugindata, {}),
 | 
			
		||||
            onlinetimemodified: submission.onlinetimemodified,
 | 
			
		||||
            timecreated: submission.timecreated,
 | 
			
		||||
            timemodified: submission.timemodified,
 | 
			
		||||
            submitted: submission.submitted,
 | 
			
		||||
            submissionstatement: submission.submissionstatement,
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the stored submissions grades from all the assignments.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with submissions grades.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getAllSubmissionsGrade(siteId?: string): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> {
 | 
			
		||||
        return this.getAssignSubmissionsGradeFormatted(undefined, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the stored submissions grades for a certain assignment.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with submissions grades.
 | 
			
		||||
     */
 | 
			
		||||
    async getAssignSubmissionsGrade(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> {
 | 
			
		||||
        return this.getAssignSubmissionsGradeFormatted({ assignid: assignId }, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convenience helper function to get stored submissions grading formatted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param conditions Query conditions.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with submissions grades.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getAssignSubmissionsGradeFormatted(
 | 
			
		||||
        conditions: SQLiteDBRecordValues = {},
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> {
 | 
			
		||||
        const db = await CoreSites.instance.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        const submissions: AddonModAssignSubmissionsGradingDBRecord[] = await db.getRecords(SUBMISSIONS_GRADES_TABLE, conditions);
 | 
			
		||||
 | 
			
		||||
        // Parse the plugin data and outcomes.
 | 
			
		||||
        return submissions.map((submission) => ({
 | 
			
		||||
            assignid: submission.assignid,
 | 
			
		||||
            userid: submission.userid,
 | 
			
		||||
            courseid: submission.courseid,
 | 
			
		||||
            grade: submission.grade,
 | 
			
		||||
            attemptnumber: submission.attemptnumber,
 | 
			
		||||
            addattempt: submission.addattempt,
 | 
			
		||||
            workflowstate: submission.workflowstate,
 | 
			
		||||
            applytoall: submission.applytoall,
 | 
			
		||||
            outcomes: CoreTextUtils.instance.parseJSON<AddonModAssignOutcomes>(submission.outcomes, {}),
 | 
			
		||||
            plugindata: CoreTextUtils.instance.parseJSON<AddonModAssignSavePluginData>(submission.plugindata, {}),
 | 
			
		||||
            timemodified: submission.timemodified,
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a stored submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with submission.
 | 
			
		||||
     */
 | 
			
		||||
    async getSubmission(assignId: number, userId?: number, siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted> {
 | 
			
		||||
        userId = userId || CoreSites.instance.getCurrentSiteUserId();
 | 
			
		||||
 | 
			
		||||
        const submissions = await this.getAssignSubmissionsFormatted({ assignid: assignId, userid: userId }, siteId);
 | 
			
		||||
 | 
			
		||||
        if (submissions.length) {
 | 
			
		||||
            return submissions[0];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new CoreError('No records found.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the path to the folder where to store files for an offline submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the path.
 | 
			
		||||
     */
 | 
			
		||||
    async getSubmissionFolder(assignId: number, userId?: number, siteId?: string): Promise<string> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        userId = userId || site.getUserId();
 | 
			
		||||
        const siteFolderPath = CoreFile.instance.getSiteFolder(site.getId());
 | 
			
		||||
        const submissionFolderPath = 'offlineassign/' + assignId + '/' + userId;
 | 
			
		||||
 | 
			
		||||
        return CoreTextUtils.instance.concatenatePaths(siteFolderPath, submissionFolderPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a stored submission grade.
 | 
			
		||||
     * Submission grades are not identified using attempt number so it can retrieve the feedback for a previous attempt.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with submission grade.
 | 
			
		||||
     */
 | 
			
		||||
    async getSubmissionGrade(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted> {
 | 
			
		||||
        userId = userId || CoreSites.instance.getCurrentSiteUserId();
 | 
			
		||||
 | 
			
		||||
        const submissions = await this.getAssignSubmissionsGradeFormatted({ assignid: assignId, userid: userId }, siteId);
 | 
			
		||||
 | 
			
		||||
        if (submissions.length) {
 | 
			
		||||
            return submissions[0];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new CoreError('No records found.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the path to the folder where to store files for a certain plugin in an offline submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param pluginName Name of the plugin. Must be unique (both in submission and feedback plugins).
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the path.
 | 
			
		||||
     */
 | 
			
		||||
    async getSubmissionPluginFolder(assignId: number, pluginName: string, userId?: number, siteId?: string): Promise<string> {
 | 
			
		||||
        const folderPath = await this.getSubmissionFolder(assignId, userId, siteId);
 | 
			
		||||
 | 
			
		||||
        return CoreTextUtils.instance.concatenatePaths(folderPath, pluginName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the assignment has something to be synced.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: whether the assignment has something to be synced.
 | 
			
		||||
     */
 | 
			
		||||
    async hasAssignOfflineData(assignId: number, siteId?: string): Promise<boolean> {
 | 
			
		||||
        const promises:
 | 
			
		||||
        Promise<AddonModAssignSubmissionsDBRecordFormatted[] | AddonModAssignSubmissionsGradingDBRecordFormatted[]>[] = [];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        promises.push(this.getAssignSubmissions(assignId, siteId));
 | 
			
		||||
        promises.push(this.getAssignSubmissionsGrade(assignId, siteId));
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const results = await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
            return results.some((result) => result.length);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // No offline data found.
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mark/Unmark a submission as being submitted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param courseId Course ID the assign belongs to.
 | 
			
		||||
     * @param submitted True to mark as submitted, false to mark as not submitted.
 | 
			
		||||
     * @param acceptStatement True to accept the submission statement, false otherwise.
 | 
			
		||||
     * @param timemodified The time the submission was last modified in online.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if marked, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    async markSubmitted(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        submitted: boolean,
 | 
			
		||||
        acceptStatement: boolean,
 | 
			
		||||
        timemodified: number,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<number> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        userId = userId || site.getUserId();
 | 
			
		||||
        let submission: AddonModAssignSubmissionsDBRecord;
 | 
			
		||||
        try {
 | 
			
		||||
            const savedSubmission: AddonModAssignSubmissionsDBRecordFormatted =
 | 
			
		||||
                await this.getSubmission(assignId, userId, site.getId());
 | 
			
		||||
            submission = Object.assign(savedSubmission, {
 | 
			
		||||
                plugindata: savedSubmission.plugindata ? JSON.stringify(savedSubmission.plugindata) : '{}',
 | 
			
		||||
                submitted: submitted ? 1 : 0, // Mark the submission.
 | 
			
		||||
                submissionstatement: acceptStatement ? 1 : 0, // Mark the submission.
 | 
			
		||||
            });
 | 
			
		||||
        } catch {
 | 
			
		||||
            // No submission, create an empty one.
 | 
			
		||||
            const now = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
            submission = {
 | 
			
		||||
                assignid: assignId,
 | 
			
		||||
                courseid: courseId,
 | 
			
		||||
                userid: userId,
 | 
			
		||||
                onlinetimemodified: timemodified,
 | 
			
		||||
                timecreated: now,
 | 
			
		||||
                timemodified: now,
 | 
			
		||||
                plugindata: '{}',
 | 
			
		||||
                submitted: submitted ? 1 : 0, // Mark the submission.
 | 
			
		||||
                submissionstatement: acceptStatement ? 1 : 0, // Mark the submission.
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await site.getDb().insertRecord(SUBMISSIONS_TABLE, submission);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save a submission to be sent later.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assignment ID.
 | 
			
		||||
     * @param courseId Course ID the assign belongs to.
 | 
			
		||||
     * @param pluginData Data to save.
 | 
			
		||||
     * @param timemodified The time the submission was last modified in online.
 | 
			
		||||
     * @param submitted True if submission has been submitted, false otherwise.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if stored, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    async saveSubmission(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        timemodified: number,
 | 
			
		||||
        submitted: boolean,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<number> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        userId = userId || site.getUserId();
 | 
			
		||||
 | 
			
		||||
        const now = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
        const entry: AddonModAssignSubmissionsDBRecord = {
 | 
			
		||||
            assignid: assignId,
 | 
			
		||||
            courseid: courseId,
 | 
			
		||||
            plugindata: pluginData ? JSON.stringify(pluginData) : '{}',
 | 
			
		||||
            userid: userId,
 | 
			
		||||
            submitted: submitted ? 1 : 0,
 | 
			
		||||
            timecreated: now,
 | 
			
		||||
            timemodified: now,
 | 
			
		||||
            onlinetimemodified: timemodified,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return await site.getDb().insertRecord(SUBMISSIONS_TABLE, entry);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save a grading to be sent later.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assign ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param courseId Course ID the assign belongs to.
 | 
			
		||||
     * @param grade Grade to submit.
 | 
			
		||||
     * @param attemptNumber Number of the attempt being graded.
 | 
			
		||||
     * @param addAttempt Admit the user to attempt again.
 | 
			
		||||
     * @param workflowState Next workflow State.
 | 
			
		||||
     * @param applyToAll If it's a team submission, whether the grade applies to all group members.
 | 
			
		||||
     * @param outcomes Object including all outcomes values. If empty, any of them will be sent.
 | 
			
		||||
     * @param pluginData Plugin data to save.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if stored, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    async submitGradingForm(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        grade: number,
 | 
			
		||||
        attemptNumber: number,
 | 
			
		||||
        addAttempt: boolean,
 | 
			
		||||
        workflowState: string,
 | 
			
		||||
        applyToAll: boolean,
 | 
			
		||||
        outcomes: AddonModAssignOutcomes,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<number> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        const now = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
        const entry: AddonModAssignSubmissionsGradingDBRecord = {
 | 
			
		||||
            assignid: assignId,
 | 
			
		||||
            userid: userId,
 | 
			
		||||
            courseid: courseId,
 | 
			
		||||
            grade: grade,
 | 
			
		||||
            attemptnumber: attemptNumber,
 | 
			
		||||
            addattempt: addAttempt ? 1 : 0,
 | 
			
		||||
            workflowstate: workflowState,
 | 
			
		||||
            applytoall: applyToAll ? 1 : 0,
 | 
			
		||||
            outcomes: outcomes ? JSON.stringify(outcomes) : '{}',
 | 
			
		||||
            plugindata: pluginData ? JSON.stringify(pluginData) : '{}',
 | 
			
		||||
            timemodified: now,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return await site.getDb().insertRecord(SUBMISSIONS_GRADES_TABLE, entry);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignOffline = makeSingleton(AddonModAssignOfflineProvider);
 | 
			
		||||
 | 
			
		||||
export type AddonModAssignSubmissionsDBRecordFormatted = Omit<AddonModAssignSubmissionsDBRecord, 'plugindata'> & {
 | 
			
		||||
    plugindata: AddonModAssignSavePluginData;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonModAssignSubmissionsGradingDBRecordFormatted =
 | 
			
		||||
    Omit<AddonModAssignSubmissionsGradingDBRecord, 'plugindata'|'outcomes'> & {
 | 
			
		||||
        plugindata: AddonModAssignSavePluginData;
 | 
			
		||||
        outcomes: AddonModAssignOutcomes;
 | 
			
		||||
    };
 | 
			
		||||
							
								
								
									
										572
									
								
								src/addons/mod/assign/services/assign-sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										572
									
								
								src/addons/mod/assign/services/assign-sync.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,572 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreSyncBlockedError } from '@classes/base-sync';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignProvider,
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
    AddonModAssignGetSubmissionStatusWSResponse,
 | 
			
		||||
    AddonModAssignSubmissionStatusOptions,
 | 
			
		||||
} from './assign';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignOffline,
 | 
			
		||||
    AddonModAssignSubmissionsDBRecordFormatted,
 | 
			
		||||
    AddonModAssignSubmissionsGradingDBRecordFormatted,
 | 
			
		||||
} from './assign-offline';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
import { CoreGradesFormattedItem, CoreGradesFormattedRow, CoreGradesHelper } from '@features/grades/services/grades-helper';
 | 
			
		||||
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
 | 
			
		||||
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to sync assigns.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModAssignSyncResult> {
 | 
			
		||||
 | 
			
		||||
    static readonly AUTO_SYNCED = 'addon_mod_assign_autom_synced';
 | 
			
		||||
    static readonly MANUAL_SYNCED = 'addon_mod_assign_manual_synced';
 | 
			
		||||
 | 
			
		||||
    protected componentTranslate: string;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('AddonModLessonSyncProvider');
 | 
			
		||||
        this.componentTranslate = CoreCourse.instance.translateModuleName('assign');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the sync ID for a certain user grade.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assign ID.
 | 
			
		||||
     * @param userId User the grade belongs to.
 | 
			
		||||
     * @return Sync ID.
 | 
			
		||||
     */
 | 
			
		||||
    getGradeSyncId(assignId: number, userId: number): string {
 | 
			
		||||
        return 'assignGrade#' + assignId + '#' + userId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convenience function to get scale selected option.
 | 
			
		||||
     *
 | 
			
		||||
     * @param options Possible options.
 | 
			
		||||
     * @param selected Selected option to search.
 | 
			
		||||
     * @return Index of the selected option.
 | 
			
		||||
     */
 | 
			
		||||
    protected getSelectedScaleId(options: string, selected: string): number {
 | 
			
		||||
        let optionsList = options.split(',');
 | 
			
		||||
 | 
			
		||||
        optionsList = optionsList.map((value) => value.trim());
 | 
			
		||||
 | 
			
		||||
        optionsList.unshift('');
 | 
			
		||||
 | 
			
		||||
        const index = options.indexOf(selected) || 0;
 | 
			
		||||
        if (index < 0) {
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return index;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if an assignment has data to synchronize.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assign ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: whether it has data to sync.
 | 
			
		||||
     */
 | 
			
		||||
    hasDataToSync(assignId: number, siteId?: string): Promise<boolean> {
 | 
			
		||||
        return AddonModAssignOffline.instance.hasAssignOfflineData(assignId, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to synchronize all the assignments in a certain site or in all sites.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID to sync. If not defined, sync all sites.
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    syncAllAssignments(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
        return this.syncOnSites('all assignments', this.syncAllAssignmentsFunc.bind(this, !!force), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync all assignments on a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @param siteId Site ID to sync. If not defined, sync all sites.
 | 
			
		||||
     * @param Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncAllAssignmentsFunc(force: boolean, siteId: string): Promise<void> {
 | 
			
		||||
        // Get all assignments that have offline data.
 | 
			
		||||
        const assignIds = await AddonModAssignOffline.instance.getAllAssigns(siteId);
 | 
			
		||||
 | 
			
		||||
        // Try to sync all assignments.
 | 
			
		||||
        await Promise.all(assignIds.map(async (assignId) => {
 | 
			
		||||
            const result = force
 | 
			
		||||
                ? await this.syncAssign(assignId, siteId)
 | 
			
		||||
                : await this.syncAssignIfNeeded(assignId, siteId);
 | 
			
		||||
 | 
			
		||||
            if (result?.updated) {
 | 
			
		||||
                CoreEvents.trigger<AddonModAssignAutoSyncData>(AddonModAssignSyncProvider.AUTO_SYNCED, {
 | 
			
		||||
                    assignId: assignId,
 | 
			
		||||
                    warnings: result.warnings,
 | 
			
		||||
                    gradesBlocked: result.gradesBlocked,
 | 
			
		||||
                }, siteId);
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync an assignment only if a certain time has passed since the last time.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assign ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the assign is synced or it doesn't need to be synced.
 | 
			
		||||
     */
 | 
			
		||||
    async syncAssignIfNeeded(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult | undefined> {
 | 
			
		||||
        const needed = await this.isSyncNeeded(assignId, siteId);
 | 
			
		||||
 | 
			
		||||
        if (needed) {
 | 
			
		||||
            return this.syncAssign(assignId, siteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to synchronize an assign.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assign ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved in success.
 | 
			
		||||
     */
 | 
			
		||||
    async syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
        this.componentTranslate = this.componentTranslate || CoreCourse.instance.translateModuleName('assign');
 | 
			
		||||
 | 
			
		||||
        if (this.isSyncing(assignId, siteId)) {
 | 
			
		||||
            // There's already a sync ongoing for this assign, return the promise.
 | 
			
		||||
            return this.getOngoingSync(assignId, siteId)!;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Verify that assign isn't blocked.
 | 
			
		||||
        if (CoreSync.instance.isBlocked(AddonModAssignProvider.COMPONENT, assignId, siteId)) {
 | 
			
		||||
            this.logger.debug('Cannot sync assign ' + assignId + ' because it is blocked.');
 | 
			
		||||
 | 
			
		||||
            throw new CoreSyncBlockedError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Try to sync assign ' + assignId + ' in site ' + siteId);
 | 
			
		||||
 | 
			
		||||
        const syncPromise = this.performSyncAssign(assignId, siteId);
 | 
			
		||||
 | 
			
		||||
        return this.addOngoingSync(assignId, syncPromise, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform the assign submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assign ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved in success.
 | 
			
		||||
     */
 | 
			
		||||
    protected async performSyncAssign(assignId: number, siteId: string): Promise<AddonModAssignSyncResult> {
 | 
			
		||||
        // Sync offline logs.
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
            CoreCourseLogHelper.instance.syncActivity(AddonModAssignProvider.COMPONENT, assignId, siteId),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const result: AddonModAssignSyncResult = {
 | 
			
		||||
            warnings: [],
 | 
			
		||||
            updated: false,
 | 
			
		||||
            gradesBlocked: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Load offline data and sync offline logs.
 | 
			
		||||
        const [submissions, grades] = await Promise.all([
 | 
			
		||||
            this.getOfflineSubmissions(assignId, siteId),
 | 
			
		||||
            this.getOfflineGrades(assignId, siteId),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        if (!submissions.length && !grades.length) {
 | 
			
		||||
            // Nothing to sync.
 | 
			
		||||
            await CoreUtils.instance.ignoreErrors(this.setSyncTime(assignId, siteId));
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!CoreApp.instance.isOnline()) {
 | 
			
		||||
            // Cannot sync in offline.
 | 
			
		||||
            throw new CoreNetworkError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid;
 | 
			
		||||
 | 
			
		||||
        const assign = await AddonModAssign.instance.getAssignmentById(courseId, assignId, { siteId });
 | 
			
		||||
 | 
			
		||||
        let promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises = promises.concat(submissions.map(async (submission) => {
 | 
			
		||||
            await this.syncSubmission(assign, submission, result.warnings, siteId);
 | 
			
		||||
 | 
			
		||||
            result.updated = true;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        promises = promises.concat(grades.map(async (grade) => {
 | 
			
		||||
            try {
 | 
			
		||||
                await this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId);
 | 
			
		||||
 | 
			
		||||
                result.updated = true;
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                if (error instanceof CoreSyncBlockedError) {
 | 
			
		||||
                    // Grade blocked, but allow finish the sync.
 | 
			
		||||
                    result.gradesBlocked.push(grade.userid);
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw error;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.allPromises(promises);
 | 
			
		||||
 | 
			
		||||
        if (result.updated) {
 | 
			
		||||
            // Data has been sent to server. Now invalidate the WS calls.
 | 
			
		||||
            await CoreUtils.instance.ignoreErrors(AddonModAssign.instance.invalidateContent(assign.cmid, courseId, siteId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sync finished, set sync time.
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(this.setSyncTime(assignId, siteId));
 | 
			
		||||
 | 
			
		||||
        // All done, return the result.
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get offline grades to be sent.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assign ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise with grades.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getOfflineGrades(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        siteId: string,
 | 
			
		||||
    ): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> {
 | 
			
		||||
        // If no offline data found, return empty array.
 | 
			
		||||
        return CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getAssignSubmissionsGrade(assignId, siteId), []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get offline submissions to be sent.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId Assign ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise with submissions.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getOfflineSubmissions(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        siteId: string,
 | 
			
		||||
    ): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> {
 | 
			
		||||
        // If no offline data found, return empty array.
 | 
			
		||||
        return CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getAssignSubmissions(assignId, siteId), []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Synchronize a submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param offlineData Submission offline data.
 | 
			
		||||
     * @param warnings List of warnings.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if success, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncSubmission(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        offlineData: AddonModAssignSubmissionsDBRecordFormatted,
 | 
			
		||||
        warnings: string[],
 | 
			
		||||
        siteId: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        const userId = offlineData.userid;
 | 
			
		||||
        const pluginData = {};
 | 
			
		||||
        const options: AddonModAssignSubmissionStatusOptions = {
 | 
			
		||||
            userId,
 | 
			
		||||
            cmId: assign.cmid,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const status = await AddonModAssign.instance.getSubmissionStatus(assign.id, options);
 | 
			
		||||
 | 
			
		||||
        const submission = AddonModAssign.instance.getSubmissionObjectFromAttempt(assign, status.lastattempt);
 | 
			
		||||
 | 
			
		||||
        if (submission && submission.timemodified != offlineData.onlinetimemodified) {
 | 
			
		||||
            // The submission was modified in Moodle, discard the submission.
 | 
			
		||||
            this.addOfflineDataDeletedWarning(
 | 
			
		||||
                warnings,
 | 
			
		||||
                this.componentTranslate,
 | 
			
		||||
                assign.name,
 | 
			
		||||
                Translate.instance.instant('addon.mod_assign.warningsubmissionmodified'),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return this.deleteSubmissionData(assign, offlineData, submission, siteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (submission?.plugins) {
 | 
			
		||||
                // Prepare plugins data.
 | 
			
		||||
                await Promise.all(submission.plugins.map((plugin) =>
 | 
			
		||||
                    AddonModAssignSubmissionDelegate.instance.preparePluginSyncData(
 | 
			
		||||
                        assign,
 | 
			
		||||
                        submission,
 | 
			
		||||
                        plugin,
 | 
			
		||||
                        offlineData,
 | 
			
		||||
                        pluginData,
 | 
			
		||||
                        siteId,
 | 
			
		||||
                    )));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Now save the submission.
 | 
			
		||||
            if (Object.keys(pluginData).length > 0) {
 | 
			
		||||
                await AddonModAssign.instance.saveSubmissionOnline(assign.id, pluginData, siteId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (assign.submissiondrafts && offlineData.submitted) {
 | 
			
		||||
                // The user submitted the assign manually. Submit it for grading.
 | 
			
		||||
                await AddonModAssign.instance.submitForGradingOnline(assign.id, !!offlineData.submissionstatement, siteId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Submission data sent, update cached data. No need to block the user for this.
 | 
			
		||||
            AddonModAssign.instance.getSubmissionStatus(assign.id, options);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!error || !CoreUtils.instance.isWebServiceError(error)) {
 | 
			
		||||
                // Local error, reject.
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
 | 
			
		||||
            this.addOfflineDataDeletedWarning(
 | 
			
		||||
                warnings,
 | 
			
		||||
                this.componentTranslate,
 | 
			
		||||
                assign.name,
 | 
			
		||||
                CoreTextUtils.instance.getErrorMessageFromError(error) || '',
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Delete the offline data.
 | 
			
		||||
        await this.deleteSubmissionData(assign, offlineData, submission, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete the submission offline data (not grades).
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assign.
 | 
			
		||||
     * @param submission Submission.
 | 
			
		||||
     * @param offlineData Offline data.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async deleteSubmissionData(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        offlineData: AddonModAssignSubmissionsDBRecordFormatted,
 | 
			
		||||
        submission?: AddonModAssignSubmission,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        // Delete the offline data.
 | 
			
		||||
        await AddonModAssignOffline.instance.deleteSubmission(assign.id, offlineData.userid, siteId);
 | 
			
		||||
 | 
			
		||||
        if (submission?.plugins){
 | 
			
		||||
            // Delete plugins data.
 | 
			
		||||
            await Promise.all(submission.plugins.map((plugin) =>
 | 
			
		||||
                AddonModAssignSubmissionDelegate.instance.deletePluginOfflineData(
 | 
			
		||||
                    assign,
 | 
			
		||||
                    submission,
 | 
			
		||||
                    plugin,
 | 
			
		||||
                    offlineData,
 | 
			
		||||
                    siteId,
 | 
			
		||||
                )));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Synchronize a submission grade.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assignment.
 | 
			
		||||
     * @param offlineData Submission grade offline data.
 | 
			
		||||
     * @param warnings List of warnings.
 | 
			
		||||
     * @param courseId Course Id.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if success, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncSubmissionGrade(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        offlineData: AddonModAssignSubmissionsGradingDBRecordFormatted,
 | 
			
		||||
        warnings: string[],
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        siteId: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        const userId = offlineData.userid;
 | 
			
		||||
        const syncId = this.getGradeSyncId(assign.id, userId);
 | 
			
		||||
        const options: AddonModAssignSubmissionStatusOptions = {
 | 
			
		||||
            userId,
 | 
			
		||||
            cmId: assign.cmid,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Check if this grade sync is blocked.
 | 
			
		||||
        if (CoreSync.instance.isBlocked(AddonModAssignProvider.COMPONENT, syncId, siteId)) {
 | 
			
		||||
            this.logger.error(`Cannot sync grade for assign ${assign.id} and user ${userId} because it is blocked.!!!!`);
 | 
			
		||||
 | 
			
		||||
            throw new CoreSyncBlockedError(Translate.instance.instant(
 | 
			
		||||
                'core.errorsyncblocked',
 | 
			
		||||
                { $a: Translate.instance.instant('addon.mod_assign.syncblockedusercomponent') },
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const status = await AddonModAssign.instance.getSubmissionStatus(assign.id, options);
 | 
			
		||||
 | 
			
		||||
        const timemodified = (status.feedback && (status.feedback.gradeddate || status.feedback.grade?.timemodified)) || 0;
 | 
			
		||||
 | 
			
		||||
        if (timemodified > offlineData.timemodified) {
 | 
			
		||||
            // The submission grade was modified in Moodle, discard it.
 | 
			
		||||
            this.addOfflineDataDeletedWarning(
 | 
			
		||||
                warnings,
 | 
			
		||||
                this.componentTranslate,
 | 
			
		||||
                assign.name,
 | 
			
		||||
                Translate.instance.instant('addon.mod_assign.warningsubmissiongrademodified'),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return AddonModAssignOffline.instance.deleteSubmissionGrade(assign.id, userId, siteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If grade has been modified from gradebook, do not use offline.
 | 
			
		||||
        const grades: CoreGradesFormattedItem[] | CoreGradesFormattedRow[] =
 | 
			
		||||
            await CoreGradesHelper.instance.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true);
 | 
			
		||||
 | 
			
		||||
        const gradeInfo = await CoreCourse.instance.getModuleBasicGradeInfo(assign.cmid, siteId);
 | 
			
		||||
 | 
			
		||||
        // Override offline grade and outcomes based on the gradebook data.
 | 
			
		||||
        grades.forEach((grade: CoreGradesFormattedItem | CoreGradesFormattedRow) => {
 | 
			
		||||
            if ('gradedategraded' in  grade && (grade.gradedategraded || 0) >= offlineData.timemodified) {
 | 
			
		||||
                if (!grade.outcomeid && !grade.scaleid) {
 | 
			
		||||
                    if (gradeInfo && gradeInfo.scale) {
 | 
			
		||||
                        offlineData.grade = this.getSelectedScaleId(gradeInfo.scale, grade.grade || '');
 | 
			
		||||
                    } else {
 | 
			
		||||
                        offlineData.grade = parseFloat(grade.grade || '');
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (gradeInfo && grade.outcomeid && AddonModAssign.instance.isOutcomesEditEnabled() && gradeInfo.outcomes) {
 | 
			
		||||
                    gradeInfo.outcomes.forEach((outcome, index) => {
 | 
			
		||||
                        if (outcome.scale && grade.itemnumber == index) {
 | 
			
		||||
                            offlineData.outcomes[grade.itemnumber] = this.getSelectedScaleId(
 | 
			
		||||
                                outcome.scale,
 | 
			
		||||
                                grade.grade || '',
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Now submit the grade.
 | 
			
		||||
            await AddonModAssign.instance.submitGradingFormOnline(
 | 
			
		||||
                assign.id,
 | 
			
		||||
                userId,
 | 
			
		||||
                offlineData.grade,
 | 
			
		||||
                offlineData.attemptnumber,
 | 
			
		||||
                !!offlineData.addattempt,
 | 
			
		||||
                offlineData.workflowstate,
 | 
			
		||||
                !!offlineData.applytoall,
 | 
			
		||||
                offlineData.outcomes,
 | 
			
		||||
                offlineData.plugindata,
 | 
			
		||||
                siteId,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Grades sent. Discard grades drafts.
 | 
			
		||||
            let promises: Promise<void | AddonModAssignGetSubmissionStatusWSResponse>[] = [];
 | 
			
		||||
            if (status.feedback && status.feedback.plugins) {
 | 
			
		||||
                promises = status.feedback.plugins.map((plugin) =>
 | 
			
		||||
                    AddonModAssignFeedbackDelegate.instance.discardPluginFeedbackData(assign.id, userId, plugin, siteId));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Update cached data.
 | 
			
		||||
            promises.push(AddonModAssign.instance.getSubmissionStatus(assign.id, options));
 | 
			
		||||
 | 
			
		||||
            await CoreUtils.instance.allPromises(promises);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!error || !CoreUtils.instance.isWebServiceError(error)) {
 | 
			
		||||
                // Local error, reject.
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
 | 
			
		||||
            this.addOfflineDataDeletedWarning(
 | 
			
		||||
                warnings,
 | 
			
		||||
                this.componentTranslate,
 | 
			
		||||
                assign.name,
 | 
			
		||||
                CoreTextUtils.instance.getErrorMessageFromError(error) || '',
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Delete the offline data.
 | 
			
		||||
        await AddonModAssignOffline.instance.deleteSubmissionGrade(assign.id, userId, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignSync = makeSingleton(AddonModAssignSyncProvider);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a assign sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModAssignSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
    courseId?: number; // Course the assign belongs to (if known).
 | 
			
		||||
    gradesBlocked: number[]; // Whether some grade couldn't be synced because it was blocked. UserId fields of the blocked grade.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to AUTO_SYNCED event.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModAssignAutoSyncData = CoreEventSiteData & {
 | 
			
		||||
    assignId: number;
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
    gradesBlocked: number[]; // Whether some grade couldn't be synced because it was blocked. UserId fields of the blocked grade.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to MANUAL_SYNCED event.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModAssignManualSyncData = AddonModAssignAutoSyncData & {
 | 
			
		||||
    context: string;
 | 
			
		||||
    submitId?: number;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										1877
									
								
								src/addons/mod/assign/services/assign.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1877
									
								
								src/addons/mod/assign/services/assign.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										151
									
								
								src/addons/mod/assign/services/database/assign.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/addons/mod/assign/services/database/assign.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,151 @@
 | 
			
		||||
// (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 { CoreSiteSchema } from '@services/sites';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database variables for AddonModAssignOfflineProvider.
 | 
			
		||||
 */
 | 
			
		||||
export const SUBMISSIONS_TABLE = 'addon_mod_assign_submissions';
 | 
			
		||||
export const SUBMISSIONS_GRADES_TABLE = 'addon_mod_assign_submissions_grading';
 | 
			
		||||
export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
    name: 'AddonModAssignOfflineProvider',
 | 
			
		||||
    version: 1,
 | 
			
		||||
    tables: [
 | 
			
		||||
        {
 | 
			
		||||
            name: SUBMISSIONS_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'assignid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'courseid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'userid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'plugindata',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'onlinetimemodified',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timecreated',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timemodified',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'submitted',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'submissionstatement',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            primaryKeys: ['assignid', 'userid'],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: SUBMISSIONS_GRADES_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'assignid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'courseid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'userid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'grade',
 | 
			
		||||
                    type: 'REAL',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'attemptnumber',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'addattempt',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'workflowstate',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'applytoall',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'outcomes',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'plugindata',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timemodified',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            primaryKeys: ['assignid', 'userid'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data about assign submissions to sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModAssignSubmissionsDBRecord = {
 | 
			
		||||
    assignid: number; // Primary key.
 | 
			
		||||
    userid: number; // Primary key.
 | 
			
		||||
    courseid: number;
 | 
			
		||||
    plugindata: string;
 | 
			
		||||
    onlinetimemodified: number;
 | 
			
		||||
    timecreated: number;
 | 
			
		||||
    timemodified: number;
 | 
			
		||||
    submitted: number;
 | 
			
		||||
    submissionstatement?: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data about assign submission grades to sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModAssignSubmissionsGradingDBRecord = {
 | 
			
		||||
    assignid: number; // Primary key.
 | 
			
		||||
    userid: number; // Primary key.
 | 
			
		||||
    courseid: number;
 | 
			
		||||
    grade: number; // Real.
 | 
			
		||||
    attemptnumber: number;
 | 
			
		||||
    addattempt: number;
 | 
			
		||||
    workflowstate: string;
 | 
			
		||||
    applytoall: number;
 | 
			
		||||
    outcomes: string;
 | 
			
		||||
    plugindata: string;
 | 
			
		||||
    timemodified: number;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										386
									
								
								src/addons/mod/assign/services/feedback-delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										386
									
								
								src/addons/mod/assign/services/feedback-delegate.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,386 @@
 | 
			
		||||
// (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 { Injectable, Type } from '@angular/core';
 | 
			
		||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
 | 
			
		||||
import { AddonModAssignDefaultFeedbackHandler } from './handlers/default-feedback';
 | 
			
		||||
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.
 | 
			
		||||
 */
 | 
			
		||||
export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Name of the type of feedback the handler supports. E.g. 'file'.
 | 
			
		||||
     */
 | 
			
		||||
    type: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Discard the draft data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    discardDraft?(assignId: number, userId: number, siteId?: string): void | Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the Component to use to display the plugin data.
 | 
			
		||||
     * 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.
 | 
			
		||||
     * @return The component (or promise resolved with component) to use, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    getComponent?(plugin: AddonModAssignPlugin): Type<unknown> | undefined | Promise<Type<unknown> | undefined>;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the draft saved data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Data (or promise resolved with the data).
 | 
			
		||||
     */
 | 
			
		||||
    getDraft?(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Record<string, unknown> | Promise<Record<string, unknown> | undefined> | undefined;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): CoreWSExternalFile[] | Promise<CoreWSExternalFile[]>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a readable name to use for the plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @return The plugin name.
 | 
			
		||||
     */
 | 
			
		||||
    getPluginName?(plugin: AddonModAssignPlugin): string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the feedback 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 feedback.
 | 
			
		||||
     * @param userId User ID of the submission.
 | 
			
		||||
     * @return Boolean (or promise resolved with boolean): whether the data has changed.
 | 
			
		||||
     */
 | 
			
		||||
    hasDataChanged?(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
        userId: number,
 | 
			
		||||
    ): boolean | Promise<boolean>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether the plugin has draft data stored.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Boolean or promise resolved with boolean: whether the plugin has draft data.
 | 
			
		||||
     */
 | 
			
		||||
    hasDraftData?(assignId: number, userId: number, siteId?: string): boolean | Promise<boolean>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
    prefetch?(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and add to pluginData the data to send to the server based on the draft data saved.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @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.
 | 
			
		||||
     */
 | 
			
		||||
    prepareFeedbackData?(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void | Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save draft data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param data The data to save.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    saveDraft?(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        data: Record<string, unknown>,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void | Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Delegate to register plugins for assign feedback.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonModAssignFeedbackHandler> {
 | 
			
		||||
 | 
			
		||||
    protected handlerNameProperty = 'type';
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected defaultHandler: AddonModAssignDefaultFeedbackHandler,
 | 
			
		||||
    ) {
 | 
			
		||||
        super('AddonModAssignFeedbackDelegate', true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Discard the draft data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async discardPluginFeedbackData(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(plugin.type, 'discardDraft', [assignId, userId, siteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the component to use for a certain feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @return Promise resolved with the component to use, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    async getComponentForPlugin(plugin: AddonModAssignPlugin): Promise<Type<unknown> | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(plugin.type, 'getComponent', [plugin]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the draft saved data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the draft data.
 | 
			
		||||
     */
 | 
			
		||||
    async getPluginDraftData<T>(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<T | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(plugin.type, 'getDraft', [assignId, 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 Promise resolved with the files.
 | 
			
		||||
     */
 | 
			
		||||
    async getPluginFiles(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<CoreWSExternalFile[]> {
 | 
			
		||||
        const files: CoreWSExternalFile[] | undefined =
 | 
			
		||||
            await this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]);
 | 
			
		||||
 | 
			
		||||
        return files || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a readable name to use for a certain feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param plugin Plugin to get the name for.
 | 
			
		||||
     * @return Human readable name.
 | 
			
		||||
     */
 | 
			
		||||
    getPluginName(plugin: AddonModAssignPlugin): string | undefined {
 | 
			
		||||
        return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [plugin]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the feedback data has changed for a certain plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign The assignment.
 | 
			
		||||
     * @param submission The submission.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param inputData Data entered by the user for the feedback.
 | 
			
		||||
     * @param userId User ID of the submission.
 | 
			
		||||
     * @return Promise resolved with true if data has changed, resolved with false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async hasPluginDataChanged(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
        userId: number,
 | 
			
		||||
    ): Promise<boolean | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(
 | 
			
		||||
            plugin.type,
 | 
			
		||||
            'hasDataChanged',
 | 
			
		||||
            [assign, submission, plugin, inputData, userId],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether the plugin has draft data stored.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with true if it has draft data.
 | 
			
		||||
     */
 | 
			
		||||
    async hasPluginDraftData(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<boolean | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(plugin.type, 'hasDraftData', [assignId, userId, siteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a feedback plugin is supported.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pluginType Type of the plugin.
 | 
			
		||||
     * @return Whether it's supported.
 | 
			
		||||
     */
 | 
			
		||||
    isPluginSupported(pluginType: string): boolean {
 | 
			
		||||
        return this.hasHandler(pluginType, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch any required data for a feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @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> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and add to pluginData the data to submit for a certain feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param pluginData Object where to store the data to send.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when data has been gathered.
 | 
			
		||||
     */
 | 
			
		||||
    async preparePluginFeedbackData(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        return await this.executeFunctionOnEnabled(
 | 
			
		||||
            plugin.type,
 | 
			
		||||
            'prepareFeedbackData',
 | 
			
		||||
            [assignId, userId, plugin, pluginData, siteId],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save draft data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assignId The assignment ID.
 | 
			
		||||
     * @param userId User ID.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param inputData Data to save.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when data has been saved.
 | 
			
		||||
     */
 | 
			
		||||
    async saveFeedbackDraft(
 | 
			
		||||
        assignId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(
 | 
			
		||||
            plugin.type,
 | 
			
		||||
            'saveDraft',
 | 
			
		||||
            [assignId, userId, plugin, inputData, siteId],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignFeedbackDelegate = makeSingleton(AddonModAssignFeedbackDelegateService);
 | 
			
		||||
							
								
								
									
										138
									
								
								src/addons/mod/assign/services/handlers/default-feedback.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/addons/mod/assign/services/handlers/default-feedback.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,138 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { AddonModAssignPlugin } from '../assign';
 | 
			
		||||
import { AddonModAssignFeedbackHandler } from '../feedback-delegate';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default handler used when a feedback plugin doesn't have a specific implementation.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedbackHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssignDefaultFeedbackHandler';
 | 
			
		||||
    type = 'default';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Discard the draft data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    discardDraft(): void {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the draft saved data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Data (or promise resolved with the data).
 | 
			
		||||
     */
 | 
			
		||||
    getDraft(): undefined {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get files used by this plugin.
 | 
			
		||||
     * The files returned by this function will be prefetched when the user prefetches the assign.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The files (or promise resolved with the files).
 | 
			
		||||
     */
 | 
			
		||||
    getPluginFiles(): CoreWSExternalFile[] {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a readable name to use for the plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @return The plugin name.
 | 
			
		||||
     */
 | 
			
		||||
    getPluginName(plugin: AddonModAssignPlugin): string {
 | 
			
		||||
        // Check if there's a translated string for the plugin.
 | 
			
		||||
        const translationId = 'addon.mod_assign_feedback_' + plugin.type + '.pluginname';
 | 
			
		||||
        const translation = Translate.instance.instant(translationId);
 | 
			
		||||
 | 
			
		||||
        if (translationId != translation) {
 | 
			
		||||
            // Translation found, use it.
 | 
			
		||||
            return translation;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fallback to WS string.
 | 
			
		||||
        if (plugin.name) {
 | 
			
		||||
            return plugin.name;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the feedback data has changed for this plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Boolean (or promise resolved with boolean): whether the data has changed.
 | 
			
		||||
     */
 | 
			
		||||
    hasDataChanged(): boolean {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether the plugin has draft data stored.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Boolean or promise resolved with boolean: whether the plugin has draft data.
 | 
			
		||||
     */
 | 
			
		||||
    hasDraftData(): boolean {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch any required data for the plugin.
 | 
			
		||||
     * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async prefetch(): Promise<void> {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and add to pluginData the data to send to the server based on the draft data saved.
 | 
			
		||||
     *
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    prepareFeedbackData(): void {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save draft data of the feedback plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    saveDraft(): void {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										201
									
								
								src/addons/mod/assign/services/handlers/default-submission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/addons/mod/assign/services/handlers/default-submission.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,201 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { AddonModAssignPlugin } from '../assign';
 | 
			
		||||
import { AddonModAssignSubmissionHandler } from '../submission-delegate';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default handler used when a submission plugin doesn't have a specific implementation.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSubmissionHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssignBaseSubmissionHandler';
 | 
			
		||||
    type = 'base';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 | Promise<boolean> {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a plugin has no data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether the plugin is empty.
 | 
			
		||||
     */
 | 
			
		||||
    isEmpty(): boolean {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Should clear temporary data for a cancelled submission.
 | 
			
		||||
     */
 | 
			
		||||
    clearTmpData(): void {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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).
 | 
			
		||||
     *
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    copySubmissionData(): void {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete any stored data for the plugin and submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    deleteOfflineData(): void {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get files used by this plugin.
 | 
			
		||||
     * The files returned by this function will be prefetched when the user prefetches the assign.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The files (or promise resolved with the files).
 | 
			
		||||
     */
 | 
			
		||||
    getPluginFiles(): CoreWSExternalFile[] {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a readable name to use for the plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @return The plugin name.
 | 
			
		||||
     */
 | 
			
		||||
    getPluginName(plugin: AddonModAssignPlugin): string {
 | 
			
		||||
        // Check if there's a translated string for the plugin.
 | 
			
		||||
        const translationId = 'addon.mod_assign_submission_' + plugin.type + '.pluginname';
 | 
			
		||||
        const translation = Translate.instance.instant(translationId);
 | 
			
		||||
 | 
			
		||||
        if (translationId != translation) {
 | 
			
		||||
            // Translation found, use it.
 | 
			
		||||
            return translation;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fallback to WS string.
 | 
			
		||||
        if (plugin.name) {
 | 
			
		||||
            return plugin.name;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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).
 | 
			
		||||
     */
 | 
			
		||||
    getSizeForCopy(): number {
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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).
 | 
			
		||||
     */
 | 
			
		||||
    getSizeForEdit(): number {
 | 
			
		||||
        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.
 | 
			
		||||
     * @param inputData Data entered by the user for the submission.
 | 
			
		||||
     * @return Boolean (or promise resolved with boolean): whether the data has changed.
 | 
			
		||||
     */
 | 
			
		||||
    hasDataChanged(): boolean {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch any required data for the plugin.
 | 
			
		||||
     * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async prefetch(): Promise<void> {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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(): void {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     *
 | 
			
		||||
     * @return If the function is async, it should return a Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    prepareSyncData(): void {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/addons/mod/assign/services/handlers/index-link.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/addons/mod/assign/services/handlers/index-link.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to treat links to assign index page.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssignIndexLinkHandler';
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('AddonModAssign', 'assign');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignIndexLinkHandler = makeSingleton(AddonModAssignIndexLinkHandlerService);
 | 
			
		||||
							
								
								
									
										32
									
								
								src/addons/mod/assign/services/handlers/list-link.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/addons/mod/assign/services/handlers/list-link.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to treat links to assign list page.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignListLinkHandlerService extends CoreContentLinksModuleListHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssignListLinkHandler';
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('AddonModAssign', 'assign');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignListLinkHandler = makeSingleton(AddonModAssignListLinkHandlerService);
 | 
			
		||||
							
								
								
									
										94
									
								
								src/addons/mod/assign/services/handlers/module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/addons/mod/assign/services/handlers/module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
			
		||||
// (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 { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { Injectable, Type } from '@angular/core';
 | 
			
		||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
 | 
			
		||||
import { AddonModAssignIndexComponent } from '../../components/index';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseModule } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { AddonModAssign } from '../assign';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to support assign modules.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignModuleHandlerService implements CoreCourseModuleHandler {
 | 
			
		||||
 | 
			
		||||
    static readonly PAGE_NAME = 'mod_assign';
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssign';
 | 
			
		||||
    modName = 'assign';
 | 
			
		||||
 | 
			
		||||
    supportedFeatures = {
 | 
			
		||||
        [CoreConstants.FEATURE_GROUPS]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_GROUPINGS]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_MOD_INTRO]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_GRADE_OUTCOMES]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_ADVANCED_GRADING]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_PLAGIARISM]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_COMMENT]: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the handler is enabled on a site level.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether or not the handler is enabled on a site level.
 | 
			
		||||
     */
 | 
			
		||||
    async isEnabled(): Promise<boolean> {
 | 
			
		||||
        return AddonModAssign.instance.isPluginEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the data required to display the module in the course contents view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module The module object.
 | 
			
		||||
     * @return Data to render the module.
 | 
			
		||||
     */
 | 
			
		||||
    getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
 | 
			
		||||
        return {
 | 
			
		||||
            icon: CoreCourse.instance.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
 | 
			
		||||
            title: module.name,
 | 
			
		||||
            class: 'addon-mod_assign-handler',
 | 
			
		||||
            showDownloadButton: true,
 | 
			
		||||
            action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
 | 
			
		||||
                options = options || {};
 | 
			
		||||
                options.params = options.params || {};
 | 
			
		||||
                Object.assign(options.params, { module });
 | 
			
		||||
                const routeParams = '/' + courseId + '/' + module.id;
 | 
			
		||||
 | 
			
		||||
                CoreNavigator.instance.navigateToSitePath(AddonModAssignModuleHandlerService.PAGE_NAME + routeParams, options);
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the component to render the module. This is needed to support singleactivity course format.
 | 
			
		||||
     * The component returned must implement CoreCourseModuleMainComponent.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The component to use, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    async getMainComponent(): Promise<Type<unknown> | undefined> {
 | 
			
		||||
        return AddonModAssignIndexComponent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignModuleHandler = makeSingleton(AddonModAssignModuleHandlerService);
 | 
			
		||||
							
								
								
									
										531
									
								
								src/addons/mod/assign/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										531
									
								
								src/addons/mod/assign/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,531 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssign,
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignProvider,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
    AddonModAssignSubmissionStatusOptions,
 | 
			
		||||
} from '../assign';
 | 
			
		||||
import { AddonModAssignSubmissionDelegate } from '../submission-delegate';
 | 
			
		||||
import { AddonModAssignFeedbackDelegate } from '../feedback-delegate';
 | 
			
		||||
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
 | 
			
		||||
import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../assign-helper';
 | 
			
		||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreFilepool } from '@services/filepool';
 | 
			
		||||
import { CoreGroups } from '@services/groups';
 | 
			
		||||
import { AddonModAssignSync, AddonModAssignSyncResult } from '../assign-sync';
 | 
			
		||||
import { CoreUser } from '@features/user/services/user';
 | 
			
		||||
import { CoreGradesHelper } from '@features/grades/services/grades-helper';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to prefetch assigns.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssign';
 | 
			
		||||
    modName = 'assign';
 | 
			
		||||
    component = AddonModAssignProvider.COMPONENT;
 | 
			
		||||
    updatesNames = /^configuration$|^.*files$|^submissions$|^grades$|^gradeitems$|^outcomes$|^comments$/;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a certain module can use core_course_check_updates to check if it has updates.
 | 
			
		||||
     * If not defined, it will assume all modules can be checked.
 | 
			
		||||
     * The modules that return false will always be shown as outdated when they're downloaded.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID the module belongs to.
 | 
			
		||||
     * @return Whether the module can use check_updates. The promise should never be rejected.
 | 
			
		||||
     */
 | 
			
		||||
    async canUseCheckUpdates(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
 | 
			
		||||
        // Teachers cannot use the WS because it doesn't check student submissions.
 | 
			
		||||
        try {
 | 
			
		||||
            const assign = await AddonModAssign.instance.getAssignment(courseId, module.id);
 | 
			
		||||
 | 
			
		||||
            const data = await AddonModAssign.instance.getSubmissions(assign.id, { cmId: module.id });
 | 
			
		||||
            if (data.canviewsubmissions) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if the user can view their own submission.
 | 
			
		||||
            await AddonModAssign.instance.getSubmissionStatus(assign.id, { cmId: module.id });
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get list of files. If not defined, we'll assume they're in module.contents.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID the module belongs to.
 | 
			
		||||
     * @return Promise resolved with the list of files.
 | 
			
		||||
     */
 | 
			
		||||
    async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
 | 
			
		||||
        const siteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const assign = await AddonModAssign.instance.getAssignment(courseId, module.id, { siteId });
 | 
			
		||||
            // Get intro files and attachments.
 | 
			
		||||
            let files = assign.introattachments || [];
 | 
			
		||||
            files = files.concat(this.getIntroFilesFromInstance(module, assign));
 | 
			
		||||
 | 
			
		||||
            // Now get the files in the submissions.
 | 
			
		||||
            const submissionData = await AddonModAssign.instance.getSubmissions(assign.id, { cmId: module.id, siteId });
 | 
			
		||||
 | 
			
		||||
            if (submissionData.canviewsubmissions) {
 | 
			
		||||
                // Teacher, get all submissions.
 | 
			
		||||
                const submissions =
 | 
			
		||||
                    await AddonModAssignHelper.instance.getSubmissionsUserData(assign, submissionData.submissions, 0, { siteId });
 | 
			
		||||
 | 
			
		||||
                // Get all the files in the submissions.
 | 
			
		||||
                const promises = submissions.map((submission) =>
 | 
			
		||||
                    this.getSubmissionFiles(assign, submission.submitid!, !!submission.blindid, siteId).then((submissionFiles) => {
 | 
			
		||||
                        files = files.concat(submissionFiles);
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
                    }).catch((error) => {
 | 
			
		||||
                        if (error && error.errorcode == 'nopermission') {
 | 
			
		||||
                            // The user does not have persmission to view this submission, ignore it.
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        throw error;
 | 
			
		||||
                    }));
 | 
			
		||||
 | 
			
		||||
                await Promise.all(promises);
 | 
			
		||||
            } else {
 | 
			
		||||
                // Student, get only his/her submissions.
 | 
			
		||||
                const userId = CoreSites.instance.getCurrentSiteUserId();
 | 
			
		||||
                const blindMarking = !!assign.blindmarking && !assign.revealidentities;
 | 
			
		||||
 | 
			
		||||
                const submissionFiles = await this.getSubmissionFiles(assign, userId, blindMarking, siteId);
 | 
			
		||||
                files = files.concat(submissionFiles);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return files;
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Error getting data, return empty list.
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get submission files.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assign.
 | 
			
		||||
     * @param submitId User ID of the submission to get.
 | 
			
		||||
     * @param blindMarking True if blind marking, false otherwise.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with array of files.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getSubmissionFiles(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submitId: number,
 | 
			
		||||
        blindMarking: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<CoreWSExternalFile[]> {
 | 
			
		||||
 | 
			
		||||
        const submissionStatus = await AddonModAssign.instance.getSubmissionStatusWithRetry(assign, {
 | 
			
		||||
            userId: submitId,
 | 
			
		||||
            isBlind: blindMarking,
 | 
			
		||||
            siteId,
 | 
			
		||||
        });
 | 
			
		||||
        const userSubmission = AddonModAssign.instance.getSubmissionObjectFromAttempt(assign, submissionStatus.lastattempt);
 | 
			
		||||
 | 
			
		||||
        if (!submissionStatus.lastattempt || !userSubmission) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const promises: Promise<CoreWSExternalFile[]>[] = [];
 | 
			
		||||
 | 
			
		||||
        if (userSubmission.plugins) {
 | 
			
		||||
            // Add submission plugin files.
 | 
			
		||||
            userSubmission.plugins.forEach((plugin) => {
 | 
			
		||||
                promises.push(AddonModAssignSubmissionDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId));
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (submissionStatus.feedback && submissionStatus.feedback.plugins) {
 | 
			
		||||
            // Add feedback plugin files.
 | 
			
		||||
            submissionStatus.feedback.plugins.forEach((plugin) => {
 | 
			
		||||
                promises.push(AddonModAssignFeedbackDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId));
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const filesLists = await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        return [].concat.apply([], filesLists);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidate the prefetched content.
 | 
			
		||||
     *
 | 
			
		||||
     * @param moduleId The module ID.
 | 
			
		||||
     * @param courseId The course ID the module belongs to.
 | 
			
		||||
     * @return Promise resolved when the data is invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateContent(moduleId: number, courseId: number): Promise<void> {
 | 
			
		||||
        await AddonModAssign.instance.invalidateContent(moduleId, courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidate WS calls needed to determine module status.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID the module belongs to.
 | 
			
		||||
     * @return Promise resolved when invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateModule(module: CoreCourseAnyModuleData): Promise<void> {
 | 
			
		||||
        return CoreCourse.instance.invalidateModule(module.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether or not the handler is enabled on a site level.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
 | 
			
		||||
     */
 | 
			
		||||
    async isEnabled(): Promise<boolean> {
 | 
			
		||||
        return AddonModAssign.instance.isPluginEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch a module.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID the module belongs to.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
 | 
			
		||||
        return this.prefetchPackage(module, courseId, this.prefetchAssign.bind(this, module, courseId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch an assignment.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID the module belongs to.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchAssign(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
 | 
			
		||||
        const userId = CoreSites.instance.getCurrentSiteUserId();
 | 
			
		||||
        courseId = courseId || module.course || CoreSites.instance.getCurrentSiteHomeId();
 | 
			
		||||
        const siteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const options: CoreSitesCommonWSOptions = {
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const modOptions: CoreCourseCommonModWSOptions = {
 | 
			
		||||
            cmId: module.id,
 | 
			
		||||
            ...options,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get assignment to retrieve all its submissions.
 | 
			
		||||
        const assign = await AddonModAssign.instance.getAssignment(courseId, module.id, options);
 | 
			
		||||
        const promises: Promise<any>[] = [];
 | 
			
		||||
        const blindMarking = assign.blindmarking && !assign.revealidentities;
 | 
			
		||||
 | 
			
		||||
        if (blindMarking) {
 | 
			
		||||
            promises.push(
 | 
			
		||||
                CoreUtils.instance.ignoreErrors(AddonModAssign.instance.getAssignmentUserMappings(assign.id, -1, modOptions)),
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        promises.push(this.prefetchSubmissions(assign, courseId, module.id, userId, siteId));
 | 
			
		||||
 | 
			
		||||
        promises.push(CoreCourseHelper.instance.getModuleCourseIdByInstance(assign.id, 'assign', siteId));
 | 
			
		||||
 | 
			
		||||
        // Download intro files and attachments. Do not call getFiles because it'd call some WS twice.
 | 
			
		||||
        let files = assign.introattachments || [];
 | 
			
		||||
        files = files.concat(this.getIntroFilesFromInstance(module, assign));
 | 
			
		||||
 | 
			
		||||
        promises.push(CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id));
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch assign submissions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assign.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param moduleId Module ID.
 | 
			
		||||
     * @param userId User ID. If not defined, site's current user.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when prefetched, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchSubmissions(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        moduleId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        siteId: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const modOptions: CoreCourseCommonModWSOptions = {
 | 
			
		||||
            cmId: moduleId,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get submissions.
 | 
			
		||||
        const submissions = await AddonModAssign.instance.getSubmissions(assign.id, modOptions);
 | 
			
		||||
        const promises: Promise<any>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(this.prefetchParticipantSubmissions(
 | 
			
		||||
            assign,
 | 
			
		||||
            submissions.canviewsubmissions,
 | 
			
		||||
            submissions.submissions,
 | 
			
		||||
            moduleId,
 | 
			
		||||
            courseId,
 | 
			
		||||
            userId,
 | 
			
		||||
            siteId,
 | 
			
		||||
        ));
 | 
			
		||||
 | 
			
		||||
        // Prefetch own submission, we need to do this for teachers too so the response with error is cached.
 | 
			
		||||
        promises.push(
 | 
			
		||||
            this.prefetchSubmission(
 | 
			
		||||
                assign,
 | 
			
		||||
                courseId,
 | 
			
		||||
                moduleId,
 | 
			
		||||
                {
 | 
			
		||||
                    userId,
 | 
			
		||||
                    readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
                    siteId,
 | 
			
		||||
                },
 | 
			
		||||
                true,
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async prefetchParticipantSubmissions(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        canviewsubmissions: boolean,
 | 
			
		||||
        submissions: AddonModAssignSubmission[] = [],
 | 
			
		||||
        moduleId: number,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        userId: number,
 | 
			
		||||
        siteId: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        const options: CoreSitesCommonWSOptions = {
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const modOptions: CoreCourseCommonModWSOptions = {
 | 
			
		||||
            cmId: moduleId,
 | 
			
		||||
            ...options,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Always prefetch groupInfo.
 | 
			
		||||
        const groupInfo = await CoreGroups.instance.getActivityGroupInfo(assign.cmid, false, undefined, siteId);
 | 
			
		||||
        if (!canviewsubmissions) {
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Teacher, prefetch all submissions.
 | 
			
		||||
        if (!groupInfo.groups || groupInfo.groups.length == 0) {
 | 
			
		||||
            groupInfo.groups = [{ id: 0, name: '' }];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const promises = groupInfo.groups.map((group) =>
 | 
			
		||||
            AddonModAssignHelper.instance.getSubmissionsUserData(assign, submissions, group.id, options)
 | 
			
		||||
                .then((submissions: AddonModAssignSubmissionFormatted[]) => {
 | 
			
		||||
 | 
			
		||||
                    const subPromises: Promise<any>[] = submissions.map((submission) => {
 | 
			
		||||
                        const submissionOptions = {
 | 
			
		||||
                            userId: submission.submitid,
 | 
			
		||||
                            groupId: group.id,
 | 
			
		||||
                            isBlind: !!submission.blindid,
 | 
			
		||||
                            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
                            siteId,
 | 
			
		||||
                        };
 | 
			
		||||
 | 
			
		||||
                        return this.prefetchSubmission(assign, courseId, moduleId, submissionOptions, true);
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    if (!assign.markingworkflow) {
 | 
			
		||||
                        // Get assignment grades only if workflow is not enabled to check grading date.
 | 
			
		||||
                        subPromises.push(AddonModAssign.instance.getAssignmentGrades(assign.id, modOptions));
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Prefetch the submission of the current user even if it does not exist, this will be create it.
 | 
			
		||||
                    if (!submissions || !submissions.find((subm: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) {
 | 
			
		||||
                        const submissionOptions = {
 | 
			
		||||
                            userId,
 | 
			
		||||
                            groupId: group.id,
 | 
			
		||||
                            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
                            siteId,
 | 
			
		||||
                        };
 | 
			
		||||
 | 
			
		||||
                        subPromises.push(this.prefetchSubmission(assign, courseId, moduleId, submissionOptions));
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return Promise.all(subPromises);
 | 
			
		||||
                }).then(async () => {
 | 
			
		||||
                    // Participiants already fetched, we don't need to ignore cache now.
 | 
			
		||||
                    const participants = await AddonModAssignHelper.instance.getParticipants(assign, group.id, { siteId });
 | 
			
		||||
 | 
			
		||||
                    // Fail silently (Moodle < 3.2).
 | 
			
		||||
                    await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
                        CoreUser.instance.prefetchUserAvatars(participants, 'profileimageurl', siteId),
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch a submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign Assign.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param moduleId Module ID.
 | 
			
		||||
     * @param options Other options, see getSubmissionStatusWithRetry.
 | 
			
		||||
     * @param resolveOnNoPermission If true, will avoid throwing if a nopermission error is raised.
 | 
			
		||||
     * @return Promise resolved when prefetched, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchSubmission(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        moduleId: number,
 | 
			
		||||
        options: AddonModAssignSubmissionStatusOptions = {},
 | 
			
		||||
        resolveOnNoPermission = false,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const submission = await AddonModAssign.instance.getSubmissionStatusWithRetry(assign, options);
 | 
			
		||||
        const siteId = options.siteId!;
 | 
			
		||||
        const userId = options.userId;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const promises: Promise<any>[] = [];
 | 
			
		||||
            const blindMarking = !!assign.blindmarking && !assign.revealidentities;
 | 
			
		||||
            let userIds: number[] = [];
 | 
			
		||||
            const userSubmission = AddonModAssign.instance.getSubmissionObjectFromAttempt(assign, submission.lastattempt);
 | 
			
		||||
 | 
			
		||||
            if (submission.lastattempt) {
 | 
			
		||||
                // Get IDs of the members who need to submit.
 | 
			
		||||
                if (!blindMarking && submission.lastattempt.submissiongroupmemberswhoneedtosubmit) {
 | 
			
		||||
                    userIds = userIds.concat(submission.lastattempt.submissiongroupmemberswhoneedtosubmit);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (userSubmission && userSubmission.id) {
 | 
			
		||||
                    // Prefetch submission plugins data.
 | 
			
		||||
                    if (userSubmission.plugins) {
 | 
			
		||||
                        userSubmission.plugins.forEach((plugin) => {
 | 
			
		||||
                            // Prefetch the plugin WS data.
 | 
			
		||||
                            promises.push(
 | 
			
		||||
                                AddonModAssignSubmissionDelegate.instance.prefetch(assign, userSubmission, plugin, siteId),
 | 
			
		||||
                            );
 | 
			
		||||
 | 
			
		||||
                            // Prefetch the plugin files.
 | 
			
		||||
                            promises.push(
 | 
			
		||||
                                AddonModAssignSubmissionDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId)
 | 
			
		||||
                                    .then((files) =>
 | 
			
		||||
                                        CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id))
 | 
			
		||||
                                    .catch(() => {
 | 
			
		||||
                                        // Ignore errors.
 | 
			
		||||
                                    }),
 | 
			
		||||
                            );
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Get ID of the user who did the submission.
 | 
			
		||||
                    if (userSubmission.userid) {
 | 
			
		||||
                        userIds.push(userSubmission.userid);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Prefetch grade items.
 | 
			
		||||
            if (userId) {
 | 
			
		||||
                promises.push(CoreCourse.instance.getModuleBasicGradeInfo(moduleId, siteId).then((gradeInfo) => {
 | 
			
		||||
                    if (gradeInfo) {
 | 
			
		||||
                        promises.push(
 | 
			
		||||
                            CoreGradesHelper.instance.getGradeModuleItems(courseId, moduleId, userId, undefined, siteId, true),
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Prefetch feedback.
 | 
			
		||||
            if (submission.feedback) {
 | 
			
		||||
                // Get profile and image of the grader.
 | 
			
		||||
                if (submission.feedback.grade && submission.feedback.grade.grader > 0) {
 | 
			
		||||
                    userIds.push(submission.feedback.grade.grader);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Prefetch feedback plugins data.
 | 
			
		||||
                if (submission.feedback.plugins && userSubmission && userSubmission.id) {
 | 
			
		||||
                    submission.feedback.plugins.forEach((plugin) => {
 | 
			
		||||
                        // Prefetch the plugin WS data.
 | 
			
		||||
                        promises.push(AddonModAssignFeedbackDelegate.instance.prefetch(assign, userSubmission, plugin, siteId));
 | 
			
		||||
 | 
			
		||||
                        // Prefetch the plugin files.
 | 
			
		||||
                        promises.push(
 | 
			
		||||
                            AddonModAssignFeedbackDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId)
 | 
			
		||||
                                .then((files) => CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id))
 | 
			
		||||
                                .catch(() => {
 | 
			
		||||
                                    // Ignore errors.
 | 
			
		||||
                                }),
 | 
			
		||||
                        );
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Prefetch user profiles.
 | 
			
		||||
            promises.push(CoreUser.instance.prefetchProfiles(userIds, courseId, siteId));
 | 
			
		||||
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Ignore if the user can't view their own submission.
 | 
			
		||||
            if (resolveOnNoPermission && error.errorcode != 'nopermission') {
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync a module.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID the module belongs to
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
 | 
			
		||||
        return AddonModAssignSync.instance.syncAssign(module.instance!, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignPrefetchHandler = makeSingleton(AddonModAssignPrefetchHandlerService);
 | 
			
		||||
							
								
								
									
										66
									
								
								src/addons/mod/assign/services/handlers/push-click.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/addons/mod/assign/services/handlers/push-click.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
 | 
			
		||||
import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate';
 | 
			
		||||
import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications';
 | 
			
		||||
import { CoreUrlUtils } from '@services/utils/url';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModAssign } from '../assign';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler for assign push notifications clicks.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignPushClickHandlerService implements CorePushNotificationsClickHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssignPushClickHandler';
 | 
			
		||||
    priority = 200;
 | 
			
		||||
    featureName = 'CoreCourseModuleDelegate_AddonModAssign';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a notification click is handled by this handler.
 | 
			
		||||
     *
 | 
			
		||||
     * @param notification The notification to check.
 | 
			
		||||
     * @return Whether the notification click is handled by this handler
 | 
			
		||||
     */
 | 
			
		||||
    async handles(notification: NotificationData): Promise<boolean> {
 | 
			
		||||
        return CoreUtils.instance.isTrueOrOne(notification.notif) && notification.moodlecomponent == 'mod_assign' &&
 | 
			
		||||
                notification.name == 'assign_notification';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle the notification click.
 | 
			
		||||
     *
 | 
			
		||||
     * @param notification The notification to check.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async handleClick(notification: NotificationData): Promise<void> {
 | 
			
		||||
        const contextUrlParams = CoreUrlUtils.instance.extractUrlParams(notification.contexturl);
 | 
			
		||||
        const courseId = Number(notification.courseid);
 | 
			
		||||
        const moduleId = Number(contextUrlParams.id);
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(AddonModAssign.instance.invalidateContent(moduleId, courseId, notification.site));
 | 
			
		||||
        await CoreCourseHelper.instance.navigateToModule(moduleId, notification.site, courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignPushClickHandler = makeSingleton(AddonModAssignPushClickHandlerService);
 | 
			
		||||
 | 
			
		||||
type NotificationData = CorePushNotificationsNotificationBasicData & {
 | 
			
		||||
    courseid: number;
 | 
			
		||||
    contexturl: string;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										50
									
								
								src/addons/mod/assign/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/addons/mod/assign/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreCronHandler } from '@services/cron';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModAssignSync } from '../assign-sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Synchronization cron handler.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignSyncCronHandlerService implements CoreCronHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModAssignSyncCronHandler';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute the process.
 | 
			
		||||
     * Receives the ID of the site affected, undefined for all sites.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId ID of the site affected, undefined for all sites.
 | 
			
		||||
     * @param force Wether the execution is forced (manual sync).
 | 
			
		||||
     * @return Promise resolved when done, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    execute(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
        return AddonModAssignSync.instance.syncAllAssignments(siteId, force);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the time between consecutive executions.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Time between consecutive executions (in ms).
 | 
			
		||||
     */
 | 
			
		||||
    getInterval(): number {
 | 
			
		||||
        return AddonModAssignSync.instance.syncInterval;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignSyncCronHandler = makeSingleton(AddonModAssignSyncCronHandlerService);
 | 
			
		||||
							
								
								
									
										566
									
								
								src/addons/mod/assign/services/submission-delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										566
									
								
								src/addons/mod/assign/services/submission-delegate.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,566 @@
 | 
			
		||||
// (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 { Injectable, Type } from '@angular/core';
 | 
			
		||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
 | 
			
		||||
import { AddonModAssignDefaultSubmissionHandler } from './handlers/default-submission';
 | 
			
		||||
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.
 | 
			
		||||
 */
 | 
			
		||||
export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Name of the type of submission the handler supports. E.g. 'file'.
 | 
			
		||||
     */
 | 
			
		||||
    type: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the
 | 
			
		||||
     * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit
 | 
			
		||||
     * unfiltered data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign The assignment.
 | 
			
		||||
     * @param submission The submission.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @return Boolean or promise resolved with boolean: whether it can be edited in offline.
 | 
			
		||||
     */
 | 
			
		||||
    canEditOffline?(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
    ): boolean | Promise<boolean>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Should clear temporary data for a cancelled submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign The assignment.
 | 
			
		||||
     * @param submission The submission.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param inputData Data entered by the user for the submission.
 | 
			
		||||
     */
 | 
			
		||||
    clearTmpData?(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
    ): void;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
    copySubmissionData?(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void | Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
    deleteOfflineData?(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        offlineData: AddonModAssignSubmissionsDBRecordFormatted,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void | Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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?: boolean,
 | 
			
		||||
    ): Type<unknown> | undefined | Promise<Type<unknown> | undefined>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): CoreWSExternalFile[] | Promise<CoreWSExternalFile[]>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a readable name to use for the plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @return The plugin name.
 | 
			
		||||
     */
 | 
			
		||||
    getPluginName?(plugin: AddonModAssignPlugin): string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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).
 | 
			
		||||
     */
 | 
			
		||||
    getSizeForCopy?(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
    ): number | Promise<number>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
    ): number | Promise<number>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
    hasDataChanged?(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
    ): boolean | Promise<boolean>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 | Promise<boolean>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
    prefetch?(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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: Record<string, unknown>,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        offline?: boolean,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void | Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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: AddonModAssignSavePluginData,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void | Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Delegate to register plugins for assign submission.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonModAssignSubmissionHandler> {
 | 
			
		||||
 | 
			
		||||
    protected handlerNameProperty = 'type';
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected defaultHandler: AddonModAssignDefaultSubmissionHandler,
 | 
			
		||||
    ) {
 | 
			
		||||
        super('AddonModAssignSubmissionDelegate', true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the plugin can be edited in offline for existing submissions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign The assignment.
 | 
			
		||||
     * @param submission The submission.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @return Promise resolved with boolean: whether it can be edited in offline.
 | 
			
		||||
     */
 | 
			
		||||
    async canPluginEditOffline(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
    ): Promise<boolean | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(plugin.type, 'canEditOffline', [assign, submission, plugin]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Clear some temporary data for a certain plugin because a submission was cancelled.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign The assignment.
 | 
			
		||||
     * @param submission The submission.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param inputData Data entered by the user for the submission.
 | 
			
		||||
     */
 | 
			
		||||
    clearTmpData(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
    ): void {
 | 
			
		||||
        return this.executeFunctionOnEnabled(plugin.type, 'clearTmpData', [assign, submission, plugin, inputData]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Copy the data from last submitted attempt to the current submission for a certain plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @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 Promise resolved when the data has been copied.
 | 
			
		||||
     */
 | 
			
		||||
    async copyPluginSubmissionData(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(
 | 
			
		||||
            plugin.type,
 | 
			
		||||
            'copySubmissionData',
 | 
			
		||||
            [assign, plugin, pluginData, userId, siteId],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete offline data stored for a certain submission and plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @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 Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async deletePluginOfflineData(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        offlineData: AddonModAssignSubmissionsDBRecordFormatted,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(
 | 
			
		||||
            plugin.type,
 | 
			
		||||
            'deleteOfflineData',
 | 
			
		||||
            [assign, submission, plugin, offlineData, siteId],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the component to use for a certain submission plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @param edit Whether the user is editing.
 | 
			
		||||
     * @return Promise resolved with the component to use, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    async getComponentForPlugin(plugin: AddonModAssignPlugin, edit?: boolean): Promise<Type<unknown> | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(plugin.type, 'getComponent', [plugin, edit]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 Promise resolved with the files.
 | 
			
		||||
     */
 | 
			
		||||
    async getPluginFiles(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<CoreWSExternalFile[]> {
 | 
			
		||||
        const files: CoreWSExternalFile[] | undefined =
 | 
			
		||||
            await this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]);
 | 
			
		||||
 | 
			
		||||
        return files || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a readable name to use for a certain submission plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @param plugin Plugin to get the name for.
 | 
			
		||||
     * @return Human readable name.
 | 
			
		||||
     */
 | 
			
		||||
    getPluginName(plugin: AddonModAssignPlugin): string | undefined {
 | 
			
		||||
        return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [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 Promise resolved with size.
 | 
			
		||||
     */
 | 
			
		||||
    async getPluginSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise<number | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(plugin.type, 'getSizeForCopy', [assign, plugin]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 Promise resolved with size.
 | 
			
		||||
     */
 | 
			
		||||
    async getPluginSizeForEdit(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
    ): Promise<number | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(
 | 
			
		||||
            plugin.type,
 | 
			
		||||
            'getSizeForEdit',
 | 
			
		||||
            [assign, submission, plugin, inputData],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the submission data has changed for a certain 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 Promise resolved with true if data has changed, resolved with false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async hasPluginDataChanged(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
    ): Promise<boolean | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(
 | 
			
		||||
            plugin.type,
 | 
			
		||||
            'hasDataChanged',
 | 
			
		||||
            [assign, submission, plugin, inputData],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a submission plugin is supported.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pluginType Type of the plugin.
 | 
			
		||||
     * @return Whether it's supported.
 | 
			
		||||
     */
 | 
			
		||||
    isPluginSupported(pluginType: string): boolean {
 | 
			
		||||
        return this.hasHandler(pluginType, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a submission plugin is supported for edit.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pluginType Type of the plugin.
 | 
			
		||||
     * @return Whether it's supported for edit.
 | 
			
		||||
     */
 | 
			
		||||
    async isPluginSupportedForEdit(pluginType: string): Promise<boolean | undefined> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(pluginType, 'isEnabledForEdit');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a plugin has no data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param assign The assignment.
 | 
			
		||||
     * @param plugin The plugin object.
 | 
			
		||||
     * @return Whether the plugin is empty.
 | 
			
		||||
     */
 | 
			
		||||
    isPluginEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean | undefined {
 | 
			
		||||
        return this.executeFunctionOnEnabled(plugin.type, 'isEmpty', [assign, plugin]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch any required data for a submission plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @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> {
 | 
			
		||||
        return await this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and add to pluginData the data to submit for a certain submission plugin.
 | 
			
		||||
     *
 | 
			
		||||
     * @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 Promise resolved when data has been gathered.
 | 
			
		||||
     */
 | 
			
		||||
    async preparePluginSubmissionData(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        inputData: Record<string, unknown>,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        offline?: boolean,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void | undefined> {
 | 
			
		||||
 | 
			
		||||
        return await this.executeFunctionOnEnabled(
 | 
			
		||||
            plugin.type,
 | 
			
		||||
            'prepareSubmissionData',
 | 
			
		||||
            [assign, submission, plugin, inputData, pluginData, offline, userId, siteId],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and add to pluginData the data to send to server to synchronize an offline submission.
 | 
			
		||||
     *
 | 
			
		||||
     * @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 Promise resolved when data has been gathered.
 | 
			
		||||
     */
 | 
			
		||||
    async preparePluginSyncData(
 | 
			
		||||
        assign: AddonModAssignAssign,
 | 
			
		||||
        submission: AddonModAssignSubmission,
 | 
			
		||||
        plugin: AddonModAssignPlugin,
 | 
			
		||||
        offlineData: AddonModAssignSubmissionsDBRecordFormatted,
 | 
			
		||||
        pluginData: AddonModAssignSavePluginData,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        return this.executeFunctionOnEnabled(
 | 
			
		||||
            plugin.type,
 | 
			
		||||
            'prepareSyncData',
 | 
			
		||||
            [assign, submission, plugin, offlineData, pluginData, siteId],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModAssignSubmissionDelegate = makeSingleton(AddonModAssignSubmissionDelegateService);
 | 
			
		||||
							
								
								
									
										47
									
								
								src/addons/mod/assign/submission/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/addons/mod/assign/submission/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
// (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: [
 | 
			
		||||
        {
 | 
			
		||||
            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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/submission/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/submission/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
    "pluginname": "Submission comments"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										107
									
								
								src/addons/mod/assign/submission/comments/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/addons/mod/assign/submission/comments/services/handler.ts
									
									
									
									
									
										Normal 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);
 | 
			
		||||
@ -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>
 | 
			
		||||
							
								
								
									
										85
									
								
								src/addons/mod/assign/submission/file/component/file.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/addons/mod/assign/submission/file/component/file.ts
									
									
									
									
									
										Normal 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 ngOnInit(): 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								src/addons/mod/assign/submission/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addons/mod/assign/submission/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
// (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: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => () => {
 | 
			
		||||
                AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionFileHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModAssignSubmissionFileComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    entryComponents: [
 | 
			
		||||
        AddonModAssignSubmissionFileComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignSubmissionFileModule {}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/submission/file/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/submission/file/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
    "pluginname": "File submissions"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										388
									
								
								src/addons/mod/assign/submission/file/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								src/addons/mod/assign/submission/file/services/handler.ts
									
									
									
									
									
										Normal 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
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
<!-- 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></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-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 ngOnInit(): 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								src/addons/mod/assign/submission/onlinetext/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/addons/mod/assign/submission/onlinetext/lang.json
									
									
									
									
									
										Normal 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."
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,47 @@
 | 
			
		||||
// (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: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => () => {
 | 
			
		||||
                AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionOnlineTextHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModAssignSubmissionOnlineTextComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    entryComponents: [
 | 
			
		||||
        AddonModAssignSubmissionOnlineTextComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModAssignSubmissionOnlineTextModule {}
 | 
			
		||||
							
								
								
									
										323
									
								
								src/addons/mod/assign/submission/onlinetext/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								src/addons/mod/assign/submission/onlinetext/services/handler.ts
									
									
									
									
									
										Normal 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.
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										27
									
								
								src/addons/mod/assign/submission/submission.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/addons/mod/assign/submission/submission.module.ts
									
									
									
									
									
										Normal 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 { }
 | 
			
		||||
@ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmdId',
 | 
			
		||||
        path: ':courseId/:cmId',
 | 
			
		||||
        loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModBookIndexPageModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
            (action)="expandDescription()" iconAction="fas-arrow-right">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
 | 
			
		||||
            [iconAction]="'far-newspaper'" (action)="gotoBlog()">
 | 
			
		||||
            iconAction="far-newspaper" (action)="gotoBlog()">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
 | 
			
		||||
            (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmdId',
 | 
			
		||||
        path: ':courseId/:cmId',
 | 
			
		||||
        loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModLessonIndexPageModule),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -257,7 +257,7 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchLesson(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean): Promise<void> {
 | 
			
		||||
        const siteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
        courseId = courseId || module.course || 1;
 | 
			
		||||
        courseId = courseId || module.course || CoreSites.instance.getCurrentSiteHomeId();
 | 
			
		||||
 | 
			
		||||
        const commonOptions = {
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreSyncBlockedError } from '@classes/base-sync';
 | 
			
		||||
import { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
@ -122,7 +122,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    syncAllLessons(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
    syncAllLessons(siteId?: string, force = false): Promise<void> {
 | 
			
		||||
        return this.syncOnSites('all lessons', this.syncAllLessonsFunc.bind(this, !!force), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -163,7 +163,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
     */
 | 
			
		||||
    async syncLessonIfNeeded(
 | 
			
		||||
        lessonId: number,
 | 
			
		||||
        askPassword?: boolean,
 | 
			
		||||
        askPassword = false,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModLessonSyncResult | undefined> {
 | 
			
		||||
        const needed = await this.isSyncNeeded(lessonId, siteId);
 | 
			
		||||
@ -184,8 +184,8 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
     */
 | 
			
		||||
    async syncLesson(
 | 
			
		||||
        lessonId: number,
 | 
			
		||||
        askPassword?: boolean,
 | 
			
		||||
        ignoreBlock?: boolean,
 | 
			
		||||
        askPassword = false,
 | 
			
		||||
        ignoreBlock = false,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModLessonSyncResult> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
@ -201,7 +201,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
        if (!ignoreBlock && CoreSync.instance.isBlocked(AddonModLessonProvider.COMPONENT, lessonId, siteId)) {
 | 
			
		||||
            this.logger.debug('Cannot sync lesson ' + lessonId + ' because it is blocked.');
 | 
			
		||||
 | 
			
		||||
            throw new CoreError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
 | 
			
		||||
            throw new CoreSyncBlockedError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Try to sync lesson ' + lessonId + ' in site ' + siteId);
 | 
			
		||||
@ -222,8 +222,8 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
     */
 | 
			
		||||
    protected async performSyncLesson(
 | 
			
		||||
        lessonId: number,
 | 
			
		||||
        askPassword?: boolean,
 | 
			
		||||
        ignoreBlock?: boolean,
 | 
			
		||||
        askPassword = false,
 | 
			
		||||
        ignoreBlock = false,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModLessonSyncResult> {
 | 
			
		||||
        // Sync offline logs.
 | 
			
		||||
@ -270,7 +270,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
    protected async syncAttempts(
 | 
			
		||||
        lessonId: number,
 | 
			
		||||
        result: AddonModLessonSyncResult,
 | 
			
		||||
        askPassword?: boolean,
 | 
			
		||||
        askPassword = false,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModLessonGetPasswordResult | undefined> {
 | 
			
		||||
        let attempts = await AddonModLessonOffline.instance.getLessonAttempts(lessonId, siteId);
 | 
			
		||||
@ -408,8 +408,8 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
        lessonId: number,
 | 
			
		||||
        result: AddonModLessonSyncResult,
 | 
			
		||||
        passwordData?: AddonModLessonGetPasswordResult,
 | 
			
		||||
        askPassword?: boolean,
 | 
			
		||||
        ignoreBlock?: boolean,
 | 
			
		||||
        askPassword = false,
 | 
			
		||||
        ignoreBlock = false,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        // Attempts sent or there was none. If there is a finished retake, send it.
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { AddonModAssignModule } from './assign/assign.module';
 | 
			
		||||
import { AddonModBookModule } from './book/book.module';
 | 
			
		||||
import { AddonModLessonModule } from './lesson/lesson.module';
 | 
			
		||||
import { AddonModPageModule } from './page/page.module';
 | 
			
		||||
@ -21,6 +22,7 @@ import { AddonModPageModule } from './page/page.module';
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [],
 | 
			
		||||
    imports: [
 | 
			
		||||
        AddonModAssignModule,
 | 
			
		||||
        AddonModBookModule,
 | 
			
		||||
        AddonModLessonModule,
 | 
			
		||||
        AddonModPageModule,
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmdId',
 | 
			
		||||
        path: ':courseId/:cmId',
 | 
			
		||||
        loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModPageIndexPageModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/grades/agg_mean.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/img/grades/agg_mean.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 341 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/grades/agg_sum.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/img/grades/agg_sum.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 318 B  | 
@ -231,7 +231,7 @@ export class CoreSyncBaseProvider<T = void> {
 | 
			
		||||
     * @param time Time to set. If not defined, current time.
 | 
			
		||||
     * @return Promise resolved when the time is set.
 | 
			
		||||
     */
 | 
			
		||||
    async setSyncTime(id: string, siteId?: string, time?: number): Promise<void> {
 | 
			
		||||
    async setSyncTime(id: string | number, siteId?: string, time?: number): Promise<void> {
 | 
			
		||||
        time = typeof time != 'undefined' ? time : Date.now();
 | 
			
		||||
 | 
			
		||||
        await CoreSync.instance.insertOrUpdateSyncRecord(this.component, id, { time: time }, siteId);
 | 
			
		||||
@ -245,7 +245,7 @@ export class CoreSyncBaseProvider<T = void> {
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async setSyncWarnings(id: string, warnings: string[], siteId?: string): Promise<void> {
 | 
			
		||||
    async setSyncWarnings(id: string | number, warnings: string[], siteId?: string): Promise<void> {
 | 
			
		||||
        const warningsText = JSON.stringify(warnings || []);
 | 
			
		||||
 | 
			
		||||
        await CoreSync.instance.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId);
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ export class CoreIonLoadingElement {
 | 
			
		||||
     * Present the loading.
 | 
			
		||||
     */
 | 
			
		||||
    async present(): Promise<void> {
 | 
			
		||||
        // Wait a bit before presenting the modal, to prevent it being displayed if dissmiss is called fast.
 | 
			
		||||
        // Wait a bit before presenting the modal, to prevent it being displayed if dismiss is called fast.
 | 
			
		||||
        await CoreUtils.instance.wait(40);
 | 
			
		||||
 | 
			
		||||
        if (!this.isDismissed) {
 | 
			
		||||
 | 
			
		||||
@ -55,8 +55,12 @@ export abstract class CorePageItemsListManager<Item> {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Process page started operations.
 | 
			
		||||
     *
 | 
			
		||||
     * @param splitView Split view component.
 | 
			
		||||
     */
 | 
			
		||||
    async start(): Promise<void> {
 | 
			
		||||
    async start(splitView: CoreSplitViewComponent): Promise<void> {
 | 
			
		||||
        this.watchSplitViewOutlet(splitView);
 | 
			
		||||
 | 
			
		||||
        // Calculate current selected item.
 | 
			
		||||
        const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent });
 | 
			
		||||
        if (route !== null && route.firstChild) {
 | 
			
		||||
 | 
			
		||||
@ -71,6 +71,7 @@ export class CoreFaIconDirective implements OnChanges {
 | 
			
		||||
        if (library != 'ionic') {
 | 
			
		||||
            const src = `assets/fonts/font-awesome/${library}/${iconName}.svg`;
 | 
			
		||||
            this.element.setAttribute('src', src);
 | 
			
		||||
            this.element.classList.add('faicon');
 | 
			
		||||
 | 
			
		||||
            if (CoreConstants.BUILD.isDevelopment || CoreConstants.BUILD.isTesting) {
 | 
			
		||||
                try {
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler';
 | 
			
		||||
/**
 | 
			
		||||
 * Base class to create activity sync providers. It provides some common functions.
 | 
			
		||||
 */
 | 
			
		||||
export class CoreCourseActivitySyncBaseProvider<T> extends CoreSyncBaseProvider<T> {
 | 
			
		||||
export class CoreCourseActivitySyncBaseProvider<T = void> extends CoreSyncBaseProvider<T> {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Conveniece function to prefetch data after an update.
 | 
			
		||||
 | 
			
		||||
@ -522,7 +522,14 @@ export class CoreCourseProvider {
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the module's grade info.
 | 
			
		||||
     */
 | 
			
		||||
    async getModuleBasicGradeInfo(moduleId: number, siteId?: string): Promise<CoreCourseModuleGradeInfo | false> {
 | 
			
		||||
    async getModuleBasicGradeInfo(moduleId: number, siteId?: string): Promise<CoreCourseModuleGradeInfo | undefined> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        if (!site || !site.isVersionGreaterEqualThan('3.2')) {
 | 
			
		||||
            // On 3.1 won't get grading info and will return undefined. See check bellow.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const info = await this.getModuleBasicInfo(moduleId, siteId);
 | 
			
		||||
 | 
			
		||||
        const grade: CoreCourseModuleGradeInfo = {
 | 
			
		||||
@ -539,10 +546,11 @@ export class CoreCourseProvider {
 | 
			
		||||
            typeof grade.advancedgrading != 'undefined' ||
 | 
			
		||||
            typeof grade.outcomes != 'undefined'
 | 
			
		||||
        ) {
 | 
			
		||||
            // On 3.1 won't get grading info and will return undefined.
 | 
			
		||||
            return grade;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1461,22 +1469,32 @@ export type CoreCourseModuleContentFile = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Course module basic info type.
 | 
			
		||||
 * Course module basic info type. 3.2 onwards.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreCourseModuleGradeInfo = {
 | 
			
		||||
    grade?: number; // Grade (max value or scale id).
 | 
			
		||||
    scale?: string; // Scale items (if used).
 | 
			
		||||
    gradepass?: string; // Grade to pass (float).
 | 
			
		||||
    gradecat?: number; // Grade category.
 | 
			
		||||
    advancedgrading?: { // Advanced grading settings.
 | 
			
		||||
        area: string; // Gradable area name.
 | 
			
		||||
        method: string; // Grading method.
 | 
			
		||||
    }[];
 | 
			
		||||
    outcomes?: { // Outcomes information.
 | 
			
		||||
        id: string; // Outcome id.
 | 
			
		||||
        name: string; // Outcome full name.
 | 
			
		||||
        scale: string; // Scale items.
 | 
			
		||||
    }[];
 | 
			
		||||
    advancedgrading?: CoreCourseModuleAdvancedGradingSetting[]; // Advanced grading settings.
 | 
			
		||||
    outcomes?: CoreCourseModuleGradeOutcome[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Advanced grading settings.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreCourseModuleAdvancedGradingSetting = {
 | 
			
		||||
    area: string; // Gradable area name.
 | 
			
		||||
    method: string; // Grading method.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Grade outcome information.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreCourseModuleGradeOutcome = {
 | 
			
		||||
    id: string; // Outcome id.
 | 
			
		||||
    name: string; // Outcome full name.
 | 
			
		||||
    scale: string; // Scale items.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
@import "~theme/globals";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    .course-icon {
 | 
			
		||||
        color: white;
 | 
			
		||||
@ -10,35 +12,10 @@
 | 
			
		||||
        transition: all 50ms ease-in-out;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ion-icon[course-color="0"] {
 | 
			
		||||
        color: var(--core-course-color-0);
 | 
			
		||||
    }
 | 
			
		||||
    ion-icon[course-color="1"] {
 | 
			
		||||
        color: var(--core-course-color-1);
 | 
			
		||||
    }
 | 
			
		||||
    ion-icon[course-color="2"] {
 | 
			
		||||
        color: var(--core-course-color-2);
 | 
			
		||||
    }
 | 
			
		||||
    ion-icon[course-color="3"] {
 | 
			
		||||
        color: var(--core-course-color-3);
 | 
			
		||||
    }
 | 
			
		||||
    ion-icon[course-color="4"] {
 | 
			
		||||
        color: var(--core-course-color-4);
 | 
			
		||||
    }
 | 
			
		||||
    ion-icon[course-color="5"] {
 | 
			
		||||
        color: var(--core-course-color-5);
 | 
			
		||||
    }
 | 
			
		||||
    ion-icon[course-color="6"] {
 | 
			
		||||
        color: var(--core-course-color-6);
 | 
			
		||||
    }
 | 
			
		||||
    ion-icon[course-color="7"] {
 | 
			
		||||
        color: var(--core-course-color-7);
 | 
			
		||||
    }
 | 
			
		||||
    ion-icon[course-color="8"] {
 | 
			
		||||
        color: var(--core-course-color-8);
 | 
			
		||||
    }
 | 
			
		||||
    ion-icon[course-color="9"] {
 | 
			
		||||
        color: var(--core-course-color-9);
 | 
			
		||||
    @for $i from 0 to length($core-course-image-background) {
 | 
			
		||||
        ion-icon[course-color="#{$i}"] {
 | 
			
		||||
            color: nth($core-course-image-background, $i + 1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ion-avatar {
 | 
			
		||||
 | 
			
		||||
@ -7,35 +7,10 @@
 | 
			
		||||
        align-self: stretch;
 | 
			
		||||
        height: calc(100% - 20px);
 | 
			
		||||
 | 
			
		||||
        &[course-color="0"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-0);
 | 
			
		||||
        }
 | 
			
		||||
        &[course-color="1"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-1);
 | 
			
		||||
        }
 | 
			
		||||
        &[course-color="2"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-2);
 | 
			
		||||
        }
 | 
			
		||||
        &[course-color="3"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-3);
 | 
			
		||||
        }
 | 
			
		||||
        &[course-color="4"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-4);
 | 
			
		||||
        }
 | 
			
		||||
        &[course-color="5"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-5);
 | 
			
		||||
        }
 | 
			
		||||
        &[course-color="6"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-6);
 | 
			
		||||
        }
 | 
			
		||||
        &[course-color="7"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-7);
 | 
			
		||||
        }
 | 
			
		||||
        &[course-color="8"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-8);
 | 
			
		||||
        }
 | 
			
		||||
        &[course-color="9"] .core-course-thumb {
 | 
			
		||||
            background: var(--core-course-color-9);
 | 
			
		||||
        @for $i from 0 to length($core-course-image-background) {
 | 
			
		||||
            &[course-color="#{$i}"] .core-course-thumb {
 | 
			
		||||
                background: nth($core-course-image-background, $i + 1);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .core-course-thumb {
 | 
			
		||||
 | 
			
		||||
@ -63,8 +63,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        await this.fetchInitialGrades();
 | 
			
		||||
 | 
			
		||||
        this.grades.watchSplitViewOutlet(this.splitView);
 | 
			
		||||
        this.grades.start();
 | 
			
		||||
        this.grades.start(this.splitView);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -42,8 +42,7 @@ export class CoreGradesCoursesPage implements OnDestroy, AfterViewInit {
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        await this.fetchInitialCourses();
 | 
			
		||||
 | 
			
		||||
        this.courses.watchSplitViewOutlet(this.splitView);
 | 
			
		||||
        this.courses.start();
 | 
			
		||||
        this.courses.start(this.splitView);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,7 @@ import { CoreMenuItem, CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service that provides some features regarding grades information.
 | 
			
		||||
@ -51,16 +52,18 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
     * @return Formatted row object.
 | 
			
		||||
     */
 | 
			
		||||
    protected formatGradeRow(tableRow: CoreGradesTableRow): CoreGradesFormattedRow {
 | 
			
		||||
        const row = {};
 | 
			
		||||
        const row: CoreGradesFormattedRow = {
 | 
			
		||||
            rowclass: '',
 | 
			
		||||
        };
 | 
			
		||||
        for (const name in tableRow) {
 | 
			
		||||
            if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
 | 
			
		||||
                let content = String(tableRow[name].content);
 | 
			
		||||
 | 
			
		||||
                if (name == 'itemname') {
 | 
			
		||||
                    this.setRowIcon(row, content);
 | 
			
		||||
                    row['link'] = this.getModuleLink(content);
 | 
			
		||||
                    row['rowclass'] += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : '';
 | 
			
		||||
                    row['rowclass'] += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
 | 
			
		||||
                    row.link = this.getModuleLink(content);
 | 
			
		||||
                    row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : '';
 | 
			
		||||
                    row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
 | 
			
		||||
 | 
			
		||||
                    content = content.replace(/<\/span>/gi, '\n');
 | 
			
		||||
                    content = CoreTextUtils.instance.cleanTags(content);
 | 
			
		||||
@ -86,20 +89,20 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
     * @return Formatted row object.
 | 
			
		||||
     */
 | 
			
		||||
    protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable {
 | 
			
		||||
        const row = {};
 | 
			
		||||
        const row: CoreGradesFormattedRowForTable = {};
 | 
			
		||||
        for (let name in tableRow) {
 | 
			
		||||
            if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
 | 
			
		||||
                let content = String(tableRow[name].content);
 | 
			
		||||
 | 
			
		||||
                if (name == 'itemname') {
 | 
			
		||||
                    row['id'] = parseInt(tableRow[name]!.id.split('_')[1], 10);
 | 
			
		||||
                    row['colspan'] = tableRow[name]!.colspan;
 | 
			
		||||
                    row['rowspan'] = (tableRow['leader'] && tableRow['leader'].rowspan) || 1;
 | 
			
		||||
                    row.id = parseInt(tableRow[name]!.id.split('_')[1], 10);
 | 
			
		||||
                    row.colspan = tableRow[name]!.colspan;
 | 
			
		||||
                    row.rowspan = (tableRow.leader && tableRow.leader.rowspan) || 1;
 | 
			
		||||
 | 
			
		||||
                    this.setRowIcon(row, content);
 | 
			
		||||
                    row['rowclass'] = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even';
 | 
			
		||||
                    row['rowclass'] += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : '';
 | 
			
		||||
                    row['rowclass'] += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
 | 
			
		||||
                    row.rowclass = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even';
 | 
			
		||||
                    row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : '';
 | 
			
		||||
                    row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
 | 
			
		||||
 | 
			
		||||
                    content = content.replace(/<\/span>/gi, '\n');
 | 
			
		||||
                    content = CoreTextUtils.instance.cleanTags(content);
 | 
			
		||||
@ -202,14 +205,14 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
     */
 | 
			
		||||
    async getGradesCourseData(grades: CoreGradesGradeOverview[]): Promise<CoreGradesGradeOverviewWithCourseData[]> {
 | 
			
		||||
        // Obtain courses from cache to prevent network requests.
 | 
			
		||||
        let coursesWereMissing;
 | 
			
		||||
        let coursesWereMissing = false;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const courses = await CoreCourses.instance.getUserCourses(undefined, undefined, CoreSitesReadingStrategy.OnlyCache);
 | 
			
		||||
            const coursesMap = CoreUtils.instance.arrayToObject(courses, 'id');
 | 
			
		||||
 | 
			
		||||
            coursesWereMissing = this.addCourseData(grades, coursesMap);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
        } catch {
 | 
			
		||||
            coursesWereMissing = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -278,7 +281,7 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
        const grades = await CoreGrades.instance.getCourseGradesTable(courseId, userId, siteId, ignoreCache);
 | 
			
		||||
 | 
			
		||||
        if (!grades) {
 | 
			
		||||
            throw new Error('Couldn\'t get grade item');
 | 
			
		||||
            throw new CoreError('Couldn\'t get grade item');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.getGradesTableRow(grades, gradeId);
 | 
			
		||||
@ -325,15 +328,15 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
        groupId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
        ignoreCache: boolean = false,
 | 
			
		||||
    ): Promise<CoreGradesFormattedItem> {
 | 
			
		||||
    ): Promise<CoreGradesFormattedItem[] | CoreGradesFormattedRow[]> {
 | 
			
		||||
        const grades = await CoreGrades.instance.getGradeItems(courseId, userId, groupId, siteId, ignoreCache);
 | 
			
		||||
 | 
			
		||||
        if (!grades) {
 | 
			
		||||
            throw new Error('Couldn\'t get grade module items');
 | 
			
		||||
            throw new CoreError('Couldn\'t get grade module items');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ('tabledata' in grades) {
 | 
			
		||||
            // Table format.
 | 
			
		||||
            // 3.1 Table format.
 | 
			
		||||
            return this.getModuleGradesTableRows(grades, moduleId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -347,18 +350,16 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
     * @param selectedGrade Selected grade label.
 | 
			
		||||
     * @return Selected grade value.
 | 
			
		||||
     */
 | 
			
		||||
    getGradeValueFromLabel(grades: CoreMenuItem[], selectedGrade: string): number {
 | 
			
		||||
    getGradeValueFromLabel(grades: CoreMenuItem[], selectedGrade?: string): number {
 | 
			
		||||
        if (!grades || !selectedGrade) {
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const x in grades) {
 | 
			
		||||
            if (grades[x].label == selectedGrade) {
 | 
			
		||||
                return grades[x].value < 0 ? 0 : grades[x].value;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        const grade = grades.find((grade) => grade.label == selectedGrade);
 | 
			
		||||
 | 
			
		||||
        return 0;
 | 
			
		||||
        return !grade || grade.value < 0
 | 
			
		||||
            ? 0
 | 
			
		||||
            : grade.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -457,15 +458,15 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
            siteId = site.id;
 | 
			
		||||
            currentUserId = site.getUserId();
 | 
			
		||||
 | 
			
		||||
            if (moduleId) {
 | 
			
		||||
                // Try to open the module grade directly. Check if it's possible.
 | 
			
		||||
                const grades = await CoreGrades.instance.isGradeItemsAvalaible(siteId);
 | 
			
		||||
            if (!moduleId) {
 | 
			
		||||
                throw new CoreError('Invalid moduleId');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                if (!grades) {
 | 
			
		||||
                    throw new Error();
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new Error();
 | 
			
		||||
            // Try to open the module grade directly. Check if it's possible.
 | 
			
		||||
            const grades = await CoreGrades.instance.isGradeItemsAvalaible(siteId);
 | 
			
		||||
 | 
			
		||||
            if (!grades) {
 | 
			
		||||
                throw new CoreError('No grades found.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
@ -476,7 +477,7 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
                const item = Array.isArray(items) && items.find((item) => moduleId == item.cmid);
 | 
			
		||||
 | 
			
		||||
                if (!item) {
 | 
			
		||||
                    throw new Error();
 | 
			
		||||
                    throw new CoreError('Grade item not found.');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Open the item directly.
 | 
			
		||||
@ -560,46 +561,49 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
     * @param text HTML where the image will be rendered.
 | 
			
		||||
     * @return Row object with the image.
 | 
			
		||||
     */
 | 
			
		||||
    protected setRowIcon(row: CoreGradesFormattedRowForTable, text: string): CoreGradesFormattedRowForTable {
 | 
			
		||||
    protected setRowIcon(
 | 
			
		||||
        row: CoreGradesFormattedRowForTable | CoreGradesFormattedRow,
 | 
			
		||||
        text: string,
 | 
			
		||||
    ): CoreGradesFormattedRowForTable {
 | 
			
		||||
        text = text.replace('%2F', '/').replace('%2f', '/');
 | 
			
		||||
 | 
			
		||||
        if (text.indexOf('/agg_mean') > -1) {
 | 
			
		||||
            row['itemtype'] = 'agg_mean';
 | 
			
		||||
            row['image'] = 'assets/img/grades/agg_mean.png';
 | 
			
		||||
            row.itemtype = 'agg_mean';
 | 
			
		||||
            row.image = 'assets/img/grades/agg_mean.png';
 | 
			
		||||
        } else if (text.indexOf('/agg_sum') > -1) {
 | 
			
		||||
            row['itemtype'] = 'agg_sum';
 | 
			
		||||
            row['image'] = 'assets/img/grades/agg_sum.png';
 | 
			
		||||
            row.itemtype = 'agg_sum';
 | 
			
		||||
            row.image = 'assets/img/grades/agg_sum.png';
 | 
			
		||||
        } else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks')  > -1) {
 | 
			
		||||
            row['itemtype'] = 'outcome';
 | 
			
		||||
            row['icon'] = 'fa-tasks';
 | 
			
		||||
            row.itemtype = 'outcome';
 | 
			
		||||
            row.icon = 'fas-chart-pie';
 | 
			
		||||
        } else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder')  > -1) {
 | 
			
		||||
            row['itemtype'] = 'category';
 | 
			
		||||
            row['icon'] = 'fa-folder';
 | 
			
		||||
            row.itemtype = 'category';
 | 
			
		||||
            row.icon = 'fas-cubes';
 | 
			
		||||
        } else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o')  > -1) {
 | 
			
		||||
            row['itemtype'] = 'manual';
 | 
			
		||||
            row['icon'] = 'fa-square-o';
 | 
			
		||||
            row.itemtype = 'manual';
 | 
			
		||||
            row.icon = 'far-square';
 | 
			
		||||
        } else if (text.indexOf('/mod/') > -1) {
 | 
			
		||||
            const module = text.match(/mod\/([^/]*)\//);
 | 
			
		||||
            if (typeof module?.[1] != 'undefined') {
 | 
			
		||||
                row['itemtype'] = 'mod';
 | 
			
		||||
                row['itemmodule'] = module[1];
 | 
			
		||||
                row['image'] = CoreCourse.instance.getModuleIconSrc(
 | 
			
		||||
                row.itemtype = 'mod';
 | 
			
		||||
                row.itemmodule = module[1];
 | 
			
		||||
                row.image = CoreCourse.instance.getModuleIconSrc(
 | 
			
		||||
                    module[1],
 | 
			
		||||
                    CoreDomUtils.instance.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            if (row['rowspan'] && row['rowspan'] > 1) {
 | 
			
		||||
                row['itemtype'] = 'category';
 | 
			
		||||
                row['icon'] = 'fa-folder';
 | 
			
		||||
            if (row.rowspan && row.rowspan > 1) {
 | 
			
		||||
                row.itemtype = 'category';
 | 
			
		||||
                row.icon = 'fas-cubes';
 | 
			
		||||
            } else if (text.indexOf('src=') > -1) {
 | 
			
		||||
                row['itemtype'] = 'unknown';
 | 
			
		||||
                row.itemtype = 'unknown';
 | 
			
		||||
                const src = text.match(/src="([^"]*)"/);
 | 
			
		||||
                row['image'] = src?.[1];
 | 
			
		||||
                row.image = src?.[1];
 | 
			
		||||
            } else if (text.indexOf('<i ') > -1) {
 | 
			
		||||
                row['itemtype'] = 'unknown';
 | 
			
		||||
                row.itemtype = 'unknown';
 | 
			
		||||
                const src = text.match(/<i class="(?:[^"]*?\s)?(fa-[a-z0-9-]+)/);
 | 
			
		||||
                row['icon'] = src ? src[1] : '';
 | 
			
		||||
                row.icon = src ? src[1] : '';
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -665,15 +669,53 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
        return Promise.resolve([]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Type guard to check if the param is a CoreGradesGradeItem.
 | 
			
		||||
     *
 | 
			
		||||
     * @param item Param to check.
 | 
			
		||||
     * @return Whether the param is a CoreGradesGradeItem.
 | 
			
		||||
     */
 | 
			
		||||
    isGradeItem(item: CoreGradesGradeItem | CoreGradesFormattedRow): item is CoreGradesGradeItem {
 | 
			
		||||
        return 'outcomeid' in item;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class CoreGradesHelper extends makeSingleton(CoreGradesHelperProvider) {}
 | 
			
		||||
 | 
			
		||||
// @todo formatted data types.
 | 
			
		||||
export type CoreGradesFormattedRow = any;
 | 
			
		||||
export type CoreGradesFormattedRowForTable = any;
 | 
			
		||||
export type CoreGradesFormattedItem = any;
 | 
			
		||||
export type CoreGradesFormattedTableColumn = any;
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedItem = CoreGradesGradeItem & {
 | 
			
		||||
    weight?: string; // Weight.
 | 
			
		||||
    grade?: string; // The grade formatted.
 | 
			
		||||
    range?: string; // Range formatted.
 | 
			
		||||
    percentage?: string; // Percentage.
 | 
			
		||||
    lettergrade?: string; // Letter grade.
 | 
			
		||||
    average?: string; // Grade average.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedRow = {
 | 
			
		||||
    icon?: string;
 | 
			
		||||
    link?: string | false;
 | 
			
		||||
    rowclass?: string;
 | 
			
		||||
    itemtype?: string;
 | 
			
		||||
    image?: string;
 | 
			
		||||
    itemmodule?: string;
 | 
			
		||||
    rowspan?: number;
 | 
			
		||||
    itemname?: string; // The item returned data.
 | 
			
		||||
    weight?: string; // Weight column.
 | 
			
		||||
    grade?: string; // Grade column.
 | 
			
		||||
    range?: string;// Range column.
 | 
			
		||||
    percentage?: string; // Percentage column.
 | 
			
		||||
    lettergrade?: string; // Lettergrade column.
 | 
			
		||||
    rank?: string; // Rank column.
 | 
			
		||||
    average?: string; // Average column.
 | 
			
		||||
    feedback?: string; // Feedback column.
 | 
			
		||||
    contributiontocoursetotal?: string; // Contributiontocoursetotal column.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedTableRow = CoreGradesFormattedTableRowFilled | CoreGradesFormattedTableRowEmpty;
 | 
			
		||||
export type CoreGradesFormattedTable = {
 | 
			
		||||
    columns: CoreGradesFormattedTableColumn[];
 | 
			
		||||
 | 
			
		||||
@ -339,8 +339,10 @@ export class CoreGradesProvider {
 | 
			
		||||
     * @return True if ws is avalaible, false otherwise.
 | 
			
		||||
     * @since  Moodle 3.2
 | 
			
		||||
     */
 | 
			
		||||
    isGradeItemsAvalaible(siteId?: string): Promise<boolean> {
 | 
			
		||||
        return CoreSites.instance.getSite(siteId).then((site) => site.wsAvailable('gradereport_user_get_grade_items'));
 | 
			
		||||
    async isGradeItemsAvalaible(siteId?: string): Promise<boolean> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        return site.wsAvailable('gradereport_user_get_grade_items');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -33,8 +33,7 @@ export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
     */
 | 
			
		||||
    ngAfterViewInit(): void {
 | 
			
		||||
        this.sections.setItems(CoreSettingsConstants.SECTIONS);
 | 
			
		||||
        this.sections.watchSplitViewOutlet(this.splitView);
 | 
			
		||||
        this.sections.start();
 | 
			
		||||
        this.sections.start(this.splitView);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -62,8 +62,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        await this.fetchInitialParticipants();
 | 
			
		||||
 | 
			
		||||
        this.participants.watchSplitViewOutlet(this.splitView);
 | 
			
		||||
        this.participants.start();
 | 
			
		||||
        this.participants.start(this.splitView);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -267,7 +267,7 @@ export class CoreCronDelegateService {
 | 
			
		||||
     * @return True if handler uses network or not defined, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected handlerUsesNetwork(name: string): boolean {
 | 
			
		||||
        if (!this.handlers[name] || this.handlers[name].usesNetwork) {
 | 
			
		||||
        if (!this.handlers[name] || !this.handlers[name].usesNetwork) {
 | 
			
		||||
            // Invalid, return default.
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -412,7 +412,7 @@ export class CoreGroupsProvider {
 | 
			
		||||
     * @param groupInfo Group info.
 | 
			
		||||
     * @return Group ID to use.
 | 
			
		||||
     */
 | 
			
		||||
    validateGroupId(groupId: number, groupInfo: CoreGroupInfo): number {
 | 
			
		||||
    validateGroupId(groupId = 0, groupInfo: CoreGroupInfo): number {
 | 
			
		||||
        if (groupId > 0 && groupInfo && groupInfo.groups && groupInfo.groups.length > 0) {
 | 
			
		||||
            // Check if the group is in the list of groups.
 | 
			
		||||
            if (groupInfo.groups.some((group) => groupId == group.id)) {
 | 
			
		||||
 | 
			
		||||
@ -243,12 +243,34 @@ export class CoreNavigatorService {
 | 
			
		||||
        return CoreUrlUtils.instance.removeUrlParams(this.previousPath || '');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Iterately get the params checking parent routes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param route Current route.
 | 
			
		||||
     * @param name Name of the parameter.
 | 
			
		||||
     * @return Value of the parameter, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    protected getRouteSnapshotParam<T = unknown>(name: string, route?: ActivatedRoute): T | undefined {
 | 
			
		||||
        if (!route?.snapshot) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const value = route.snapshot.queryParams[name] ?? route.snapshot.params[name];
 | 
			
		||||
 | 
			
		||||
        if (typeof value != 'undefined') {
 | 
			
		||||
            return value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.getRouteSnapshotParam(name, route.parent || undefined);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a parameter for the current route.
 | 
			
		||||
     * Please notice that objects can only be retrieved once. You must call this function only once per page and parameter,
 | 
			
		||||
     * unless there's a new navigation to the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param name Name of the parameter.
 | 
			
		||||
     * @param params Optional params to get the value from. If missing, it will autodetect.
 | 
			
		||||
     * @return Value of the parameter, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    getRouteParam<T = unknown>(name: string, params?: Params): T | undefined {
 | 
			
		||||
@ -256,15 +278,16 @@ export class CoreNavigatorService {
 | 
			
		||||
 | 
			
		||||
        if (!params) {
 | 
			
		||||
            const route = this.getCurrentRoute();
 | 
			
		||||
            if (!route.snapshot) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            value = route.snapshot.queryParams[name] ?? route.snapshot.params[name];
 | 
			
		||||
            value = this.getRouteSnapshotParam(name, route);
 | 
			
		||||
        } else {
 | 
			
		||||
            value = params[name];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (typeof value == 'undefined') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let storedParam = this.storedParams[value];
 | 
			
		||||
 | 
			
		||||
        // Remove the parameter from our map if it's in there.
 | 
			
		||||
@ -286,6 +309,7 @@ export class CoreNavigatorService {
 | 
			
		||||
     * Angular router automatically converts numbers to string, this function automatically converts it back to number.
 | 
			
		||||
     *
 | 
			
		||||
     * @param name Name of the parameter.
 | 
			
		||||
     * @param params Optional params to get the value from. If missing, it will autodetect.
 | 
			
		||||
     * @return Value of the parameter, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    getRouteNumberParam(name: string, params?: Params): number | undefined {
 | 
			
		||||
@ -299,6 +323,7 @@ export class CoreNavigatorService {
 | 
			
		||||
     * Angular router automatically converts booleans to string, this function automatically converts it back to boolean.
 | 
			
		||||
     *
 | 
			
		||||
     * @param name Name of the parameter.
 | 
			
		||||
     * @param params Optional params to get the value from. If missing, it will autodetect.
 | 
			
		||||
     * @return Value of the parameter, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    getRouteBooleanParam(name: string, params?: Params): boolean | undefined {
 | 
			
		||||
@ -355,6 +380,11 @@ export class CoreNavigatorService {
 | 
			
		||||
        // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
 | 
			
		||||
        // @todo this.location.replaceState('');
 | 
			
		||||
 | 
			
		||||
        options = {
 | 
			
		||||
            preferCurrentTab: true,
 | 
			
		||||
            ...options,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        path = path.replace(/^(\.|\/main)?\//, '');
 | 
			
		||||
 | 
			
		||||
        const pathRoot = /^[^/]+/.exec(path)?.[0] ?? '';
 | 
			
		||||
@ -364,7 +394,7 @@ export class CoreNavigatorService {
 | 
			
		||||
            false,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (options.preferCurrentTab === false && isMainMenuTab) {
 | 
			
		||||
        if (!options.preferCurrentTab && isMainMenuTab) {
 | 
			
		||||
            return this.navigate(`/main/${path}`, options);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user