commit
fc39c3e30e
|
@ -51,8 +51,7 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
||||||
async ngAfterViewInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
await this.fetchInitialBadges();
|
await this.fetchInitialBadges();
|
||||||
|
|
||||||
this.badges.watchSplitViewOutlet(this.splitView);
|
this.badges.start(this.splitView);
|
||||||
this.badges.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,11 +19,13 @@ import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
|
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
|
||||||
|
|
||||||
import { AddonCalendarEditEventPage } from './edit-event.page';
|
import { AddonCalendarEditEventPage } from './edit-event.page';
|
||||||
|
import { CanLeaveGuard } from '@guards/can-leave';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: AddonCalendarEditEventPage,
|
component: AddonCalendarEditEventPage,
|
||||||
|
canDeactivate: [CanLeaveGuard],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
import { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync';
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
@ -27,9 +27,9 @@ import {
|
||||||
import { AddonCalendarOffline } from './calendar-offline';
|
import { AddonCalendarOffline } from './calendar-offline';
|
||||||
import { AddonCalendarHelper } from './calendar-helper';
|
import { AddonCalendarHelper } from './calendar-helper';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreError } from '@classes/errors/error';
|
|
||||||
import { CoreSync } from '@services/sync';
|
import { CoreSync } from '@services/sync';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to sync calendar.
|
* 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.
|
* @param force Wether to force sync not depending on last execution.
|
||||||
* @return Promise resolved if sync is successful, rejected if sync fails.
|
* @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);
|
await this.syncOnSites('all calendar events', this.syncAllEventsFunc.bind(this, [force]), siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync all events on a site.
|
* Sync all events on a site.
|
||||||
*
|
*
|
||||||
* @param siteId Site ID to sync.
|
|
||||||
* @param force Wether to force sync not depending on last execution.
|
* @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.
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
*/
|
*/
|
||||||
protected async syncAllEventsFunc(siteId: string, force?: boolean): Promise<void> {
|
protected async syncAllEventsFunc(force = false, siteId?: string): Promise<void> {
|
||||||
const result = await (force ? this.syncEvents(siteId) : this.syncEventsIfNeeded(siteId));
|
const result = force
|
||||||
|
? await this.syncEvents(siteId)
|
||||||
|
: await this.syncEventsIfNeeded(siteId);
|
||||||
|
|
||||||
if (result && result.updated) {
|
if (result?.updated) {
|
||||||
// Sync successful, send event.
|
// Sync successful, send event.
|
||||||
CoreEvents.trigger<AddonCalendarSyncEvents>(AddonCalendarSyncProvider.AUTO_SYNCED, result, siteId);
|
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.
|
* @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.
|
* @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();
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
const needed = await this.isSyncNeeded(AddonCalendarSyncProvider.SYNC_ID, siteId);
|
const needed = await this.isSyncNeeded(AddonCalendarSyncProvider.SYNC_ID, siteId);
|
||||||
|
|
||||||
if (needed) {
|
if (needed) {
|
||||||
await this.syncEvents(siteId);
|
return this.syncEvents(siteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,17 +127,12 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
|
||||||
updated: false,
|
updated: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let eventIds: number[] = [];
|
const eventIds: number[] = await CoreUtils.instance.ignoreErrors(AddonCalendarOffline.instance.getAllEventsIds(siteId), []);
|
||||||
try {
|
|
||||||
eventIds = await AddonCalendarOffline.instance.getAllEventsIds(siteId);
|
|
||||||
} catch {
|
|
||||||
// No offline data found.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventIds.length > 0) {
|
if (eventIds.length > 0) {
|
||||||
if (!CoreApp.instance.isOnline()) {
|
if (!CoreApp.instance.isOnline()) {
|
||||||
// Cannot sync in offline.
|
// Cannot sync in offline.
|
||||||
throw new CoreError('Cannot sync while offline');
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = eventIds.map((eventId) => this.syncOfflineEvent(eventId, result, siteId));
|
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)) {
|
if (CoreSync.instance.isBlocked(AddonCalendarProvider.COMPONENT, eventId, siteId)) {
|
||||||
this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.');
|
this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.');
|
||||||
|
|
||||||
throw Translate.instance.instant(
|
throw new CoreSyncBlockedError(Translate.instance.instant(
|
||||||
'core.errorsyncblocked',
|
'core.errorsyncblocked',
|
||||||
{ $a: Translate.instance.instant('addon.calendar.calendarevent') },
|
{ $a: Translate.instance.instant('addon.calendar.calendarevent') },
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// First of all, check if the event has been deleted.
|
// 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 { AddonMessagesSendMessageUserHandler } from './services/handlers/user-send-message';
|
||||||
import { Network, NgZone } from '@singletons';
|
import { Network, NgZone } from '@singletons';
|
||||||
import { AddonMessagesSync } from './services/messages-sync';
|
import { AddonMessagesSync } from './services/messages-sync';
|
||||||
|
import { AddonMessagesSyncCronHandler } from './services/handlers/sync-cron';
|
||||||
|
|
||||||
const mainMenuChildrenRoutes: Routes = [
|
const mainMenuChildrenRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -61,7 +62,7 @@ const mainMenuChildrenRoutes: Routes = [
|
||||||
// Register handlers.
|
// Register handlers.
|
||||||
CoreMainMenuDelegate.instance.registerHandler(AddonMessagesMainMenuHandler.instance);
|
CoreMainMenuDelegate.instance.registerHandler(AddonMessagesMainMenuHandler.instance);
|
||||||
CoreCronDelegate.instance.register(AddonMessagesMainMenuHandler.instance);
|
CoreCronDelegate.instance.register(AddonMessagesMainMenuHandler.instance);
|
||||||
CoreCronDelegate.instance.register(AddonMessagesPushClickHandler.instance);
|
CoreCronDelegate.instance.register(AddonMessagesSyncCronHandler.instance);
|
||||||
CoreSettingsDelegate.instance.registerHandler(AddonMessagesSettingsHandler.instance);
|
CoreSettingsDelegate.instance.registerHandler(AddonMessagesSettingsHandler.instance);
|
||||||
CoreContentLinksDelegate.instance.registerHandler(AddonMessagesIndexLinkHandler.instance);
|
CoreContentLinksDelegate.instance.registerHandler(AddonMessagesIndexLinkHandler.instance);
|
||||||
CoreContentLinksDelegate.instance.registerHandler(AddonMessagesDiscussionLinkHandler.instance);
|
CoreContentLinksDelegate.instance.registerHandler(AddonMessagesDiscussionLinkHandler.instance);
|
||||||
|
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
|
@ -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 || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"pluginname": "Feedback comments"
|
||||||
|
}
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"pluginname": "Annotate PDF"
|
||||||
|
}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"pluginname": "File feedback"
|
||||||
|
}
|
|
@ -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);
|
|
@ -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"
|
||||||
|
}
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
|
@ -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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"pluginname": "Submission comments"
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from '@addons/mod/assign/services/assign';
|
||||||
|
import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate';
|
||||||
|
import { Injectable, Type } from '@angular/core';
|
||||||
|
import { CoreComments } from '@features/comments/services/comments';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModAssignSubmissionCommentsComponent } from '../component/comments';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for comments submission plugin.
|
||||||
|
*/
|
||||||
|
@Injectable( { providedIn: 'root' })
|
||||||
|
export class AddonModAssignSubmissionCommentsHandlerService implements AddonModAssignSubmissionHandler {
|
||||||
|
|
||||||
|
name = 'AddonModAssignSubmissionCommentsHandler';
|
||||||
|
type = 'comments';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the
|
||||||
|
* plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit
|
||||||
|
* unfiltered data.
|
||||||
|
*
|
||||||
|
* @return Boolean or promise resolved with boolean: whether it can be edited in offline.
|
||||||
|
*/
|
||||||
|
canEditOffline(): boolean {
|
||||||
|
// This plugin is read only, but return true to prevent blocking the edition.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Component to use to display the plugin data, either in read or in edit mode.
|
||||||
|
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||||
|
*
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param edit Whether the user is editing.
|
||||||
|
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(plugin: AddonModAssignPlugin, edit = false): Type<unknown> | undefined {
|
||||||
|
return edit ? undefined : AddonModAssignSubmissionCommentsComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled for edit on a site level.
|
||||||
|
*
|
||||||
|
* @return Whether or not the handler is enabled for edit on a site level.
|
||||||
|
*/
|
||||||
|
isEnabledForEdit(): boolean{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch any required data for the plugin.
|
||||||
|
* This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async prefetch(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
// Fail silently (Moodle < 3.1.1, 3.2)
|
||||||
|
await CoreUtils.instance.ignoreErrors(
|
||||||
|
CoreComments.instance.getComments(
|
||||||
|
'module',
|
||||||
|
assign.cmid,
|
||||||
|
'assignsubmission_comments',
|
||||||
|
submission.id,
|
||||||
|
'submission_comments',
|
||||||
|
0,
|
||||||
|
siteId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export const AddonModAssignSubmissionCommentsHandler = makeSingleton(AddonModAssignSubmissionCommentsHandlerService);
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!-- Read only. -->
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="files && files.length && !edit">
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ plugin.name }}</h2>
|
||||||
|
<div lines="none">
|
||||||
|
<core-files [files]="files" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-files>
|
||||||
|
</div>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Edit -->
|
||||||
|
<div *ngIf="edit">
|
||||||
|
<ion-item-divider class="ion-text-wrap" sticky="true">
|
||||||
|
<ion-label><h2>{{ plugin.name }}</h2></ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
<core-attachments [files]="files" [maxSize]="maxSize" [maxSubmissions]="maxSubmissions"
|
||||||
|
[component]="component" [componentId]="assign.cmid" [acceptedTypes]="acceptedTypes" [allowOffline]="allowOffline">
|
||||||
|
</core-attachments>
|
||||||
|
</div>
|
|
@ -0,0 +1,85 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin';
|
||||||
|
import { AddonModAssign, AddonModAssignProvider } from '@addons/mod/assign/services/assign';
|
||||||
|
import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper';
|
||||||
|
import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||||
|
import { CoreFileSession } from '@services/file-session';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { AddonModAssignSubmissionFileHandlerService } from '../services/handler';
|
||||||
|
import { FileEntry } from '@ionic-native/file/ngx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render a file submission plugin.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-assign-submission-file',
|
||||||
|
templateUrl: 'addon-mod-assign-submission-file.html',
|
||||||
|
})
|
||||||
|
export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmissionPluginComponent implements OnInit {
|
||||||
|
|
||||||
|
component = AddonModAssignProvider.COMPONENT;
|
||||||
|
|
||||||
|
maxSize?: number;
|
||||||
|
acceptedTypes?: string;
|
||||||
|
maxSubmissions?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
async 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"pluginname": "File submissions"
|
||||||
|
}
|
|
@ -0,0 +1,388 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import {
|
||||||
|
AddonModAssignAssign,
|
||||||
|
AddonModAssignSubmission,
|
||||||
|
AddonModAssignPlugin,
|
||||||
|
AddonModAssignProvider,
|
||||||
|
AddonModAssign,
|
||||||
|
} from '@addons/mod/assign/services/assign';
|
||||||
|
import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper';
|
||||||
|
import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } from '@addons/mod/assign/services/assign-offline';
|
||||||
|
import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate';
|
||||||
|
import { Injectable, Type } from '@angular/core';
|
||||||
|
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||||
|
import { CoreFileHelper } from '@services/file-helper';
|
||||||
|
import { CoreFileSession } from '@services/file-session';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModAssignSubmissionFileComponent } from '../component/file';
|
||||||
|
import { FileEntry } from '@ionic-native/file/ngx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for file submission plugin.
|
||||||
|
*/
|
||||||
|
@Injectable( { providedIn: 'root' })
|
||||||
|
export class AddonModAssignSubmissionFileHandlerService implements AddonModAssignSubmissionHandler {
|
||||||
|
|
||||||
|
static readonly FOLDER_NAME = 'submission_file';
|
||||||
|
|
||||||
|
name = 'AddonModAssignSubmissionFileHandler';
|
||||||
|
type = 'file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the
|
||||||
|
* plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit
|
||||||
|
* unfiltered data.
|
||||||
|
*
|
||||||
|
* @return Boolean or promise resolved with boolean: whether it can be edited in offline.
|
||||||
|
*/
|
||||||
|
canEditOffline(): boolean {
|
||||||
|
// This plugin doesn't use Moodle filters, it can be edited in offline.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a plugin has no data.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @return Whether the plugin is empty.
|
||||||
|
*/
|
||||||
|
isEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean {
|
||||||
|
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
|
||||||
|
|
||||||
|
return files.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should clear temporary data for a cancelled submission.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
*/
|
||||||
|
clearTmpData(assign: AddonModAssignAssign): void {
|
||||||
|
const files = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
|
||||||
|
|
||||||
|
// Clear the files in session for this assign.
|
||||||
|
CoreFileSession.instance.clearFiles(AddonModAssignProvider.COMPONENT, assign.id);
|
||||||
|
|
||||||
|
// Now delete the local files from the tmp folder.
|
||||||
|
CoreFileUploader.instance.clearTmpFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will be called when the user wants to create a new submission based on the previous one.
|
||||||
|
* It should add to pluginData the data to send to server based in the data in plugin (previous attempt).
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param pluginData Object where to store the data to send.
|
||||||
|
* @return If the function is async, it should return a Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async copySubmissionData(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
pluginData: AddonModAssignSubmissionFilePluginData,
|
||||||
|
): Promise<void> {
|
||||||
|
// We need to re-upload all the existing files.
|
||||||
|
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
|
||||||
|
|
||||||
|
// Get the itemId.
|
||||||
|
pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadFiles(assign.id, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Component to use to display the plugin data, either in read or in edit mode.
|
||||||
|
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||||
|
*
|
||||||
|
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(): Type<unknown> {
|
||||||
|
return AddonModAssignSubmissionFileComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete any stored data for the plugin and submission.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param offlineData Offline data stored.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return If the function is async, it should return a Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async deleteOfflineData(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
await CoreUtils.instance.ignoreErrors(
|
||||||
|
AddonModAssignHelper.instance.deleteStoredSubmissionFiles(
|
||||||
|
assign.id,
|
||||||
|
AddonModAssignSubmissionFileHandlerService.FOLDER_NAME,
|
||||||
|
submission.userid,
|
||||||
|
siteId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get files used by this plugin.
|
||||||
|
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return The files (or promise resolved with the files).
|
||||||
|
*/
|
||||||
|
getPluginFiles(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
): CoreWSExternalFile[] {
|
||||||
|
return AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @return The size (or promise resolved with size).
|
||||||
|
*/
|
||||||
|
async getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise<number> {
|
||||||
|
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
|
||||||
|
|
||||||
|
return CoreFileHelper.instance.getTotalFilesSize(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @return The size (or promise resolved with size).
|
||||||
|
*/
|
||||||
|
async getSizeForEdit(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
): Promise<number> {
|
||||||
|
// Check if there's any change.
|
||||||
|
if (this.hasDataChanged(assign, submission, plugin)) {
|
||||||
|
const files = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
|
||||||
|
|
||||||
|
return CoreFileHelper.instance.getTotalFilesSize(files);
|
||||||
|
} else {
|
||||||
|
// Nothing has changed, we won't upload any file.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the submission data has changed for this plugin.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @return Boolean (or promise resolved with boolean): whether the data has changed.
|
||||||
|
*/
|
||||||
|
async hasDataChanged(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const offlineData = await CoreUtils.instance.ignoreErrors(
|
||||||
|
// Check if there's any offline data.
|
||||||
|
AddonModAssignOffline.instance.getSubmission(assign.id, submission.userid),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
let numFiles: number;
|
||||||
|
if (offlineData && offlineData.plugindata && offlineData.plugindata.files_filemanager) {
|
||||||
|
const offlineDataFiles = <CoreFileUploaderStoreFilesResult>offlineData.plugindata.files_filemanager;
|
||||||
|
// Has offline data, return the number of files.
|
||||||
|
numFiles = offlineDataFiles.offline + offlineDataFiles.online.length;
|
||||||
|
} else {
|
||||||
|
// No offline data, return the number of online files.
|
||||||
|
const pluginFiles = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
|
||||||
|
|
||||||
|
numFiles = pluginFiles && pluginFiles.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFiles = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
|
||||||
|
|
||||||
|
if (currentFiles.length != numFiles) {
|
||||||
|
// Number of files has changed.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await this.getSubmissionFilesToSync(assign, submission, offlineData);
|
||||||
|
|
||||||
|
// Check if there is any local file added and list has changed.
|
||||||
|
return CoreFileUploader.instance.areFileListDifferent(currentFiles, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled for edit on a site level.
|
||||||
|
*
|
||||||
|
* @return Whether or not the handler is enabled for edit on a site level.
|
||||||
|
*/
|
||||||
|
isEnabledForEdit(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare and add to pluginData the data to send to the server based on the input data.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param inputData Data entered by the user for the submission.
|
||||||
|
* @param pluginData Object where to store the data to send.
|
||||||
|
* @param offline Whether the user is editing in offline.
|
||||||
|
* @param userId User ID. If not defined, site's current user.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return If the function is async, it should return a Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async prepareSubmissionData(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
inputData: AddonModAssignSubmissionFileData,
|
||||||
|
pluginData: AddonModAssignSubmissionFilePluginData,
|
||||||
|
offline?: boolean,
|
||||||
|
userId?: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
const changed = await this.hasDataChanged(assign, submission, plugin);
|
||||||
|
if (!changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data has changed, we need to upload new files and re-upload all the existing files.
|
||||||
|
const currentFiles = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
|
||||||
|
const error = CoreUtils.instance.hasRepeatedFilenames(currentFiles);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadOrStoreFiles(
|
||||||
|
assign.id,
|
||||||
|
AddonModAssignSubmissionFileHandlerService.FOLDER_NAME,
|
||||||
|
currentFiles,
|
||||||
|
offline,
|
||||||
|
userId,
|
||||||
|
siteId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare and add to pluginData the data to send to the server based on the offline data stored.
|
||||||
|
* This will be used when performing a synchronization.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param offlineData Offline data stored.
|
||||||
|
* @param pluginData Object where to store the data to send.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return If the function is async, it should return a Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async prepareSyncData(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
|
||||||
|
pluginData: AddonModAssignSubmissionFilePluginData,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
const files = await this.getSubmissionFilesToSync(assign, submission, offlineData, siteId);
|
||||||
|
|
||||||
|
if (files.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadFiles(assign.id, files, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file list to be synced.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param offlineData Offline data stored.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return File entries when is all resolved.
|
||||||
|
*/
|
||||||
|
protected async getSubmissionFilesToSync(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
offlineData?: AddonModAssignSubmissionsDBRecordFormatted,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<(FileEntry | CoreWSExternalFile)[]> {
|
||||||
|
const filesData = <CoreFileUploaderStoreFilesResult>offlineData?.plugindata.files_filemanager;
|
||||||
|
if (!filesData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has some data to sync.
|
||||||
|
let files: (FileEntry | CoreWSExternalFile)[] = filesData.online || [];
|
||||||
|
|
||||||
|
if (filesData.offline) {
|
||||||
|
// Has offline files, get them and add them to the list.
|
||||||
|
const storedFiles = <FileEntry[]> await CoreUtils.instance.ignoreErrors(
|
||||||
|
AddonModAssignHelper.instance.getStoredSubmissionFiles(
|
||||||
|
assign.id,
|
||||||
|
AddonModAssignSubmissionFileHandlerService.FOLDER_NAME,
|
||||||
|
submission.userid,
|
||||||
|
siteId,
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
files = files.concat(storedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export const AddonModAssignSubmissionFileHandler = makeSingleton(AddonModAssignSubmissionFileHandlerService);
|
||||||
|
|
||||||
|
// Define if ever used.
|
||||||
|
export type AddonModAssignSubmissionFileData = Record<string, unknown>;
|
||||||
|
|
||||||
|
export type AddonModAssignSubmissionFilePluginData = {
|
||||||
|
// The id of a draft area containing files for this submission. Or the offline file results.
|
||||||
|
files_filemanager: number | CoreFileUploaderStoreFilesResult; // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
};
|
|
@ -0,0 +1,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -0,0 +1,323 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import {
|
||||||
|
AddonModAssignAssign,
|
||||||
|
AddonModAssignSubmission,
|
||||||
|
AddonModAssignPlugin,
|
||||||
|
AddonModAssign,
|
||||||
|
} from '@addons/mod/assign/services/assign';
|
||||||
|
import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper';
|
||||||
|
import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } from '@addons/mod/assign/services/assign-offline';
|
||||||
|
import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate';
|
||||||
|
import { Injectable, Type } from '@angular/core';
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreFileHelper } from '@services/file-helper';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
import { AddonModAssignSubmissionOnlineTextComponent } from '../component/onlinetext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for online text submission plugin.
|
||||||
|
*/
|
||||||
|
@Injectable( { providedIn: 'root' })
|
||||||
|
export class AddonModAssignSubmissionOnlineTextHandlerService implements AddonModAssignSubmissionHandler {
|
||||||
|
|
||||||
|
name = 'AddonModAssignSubmissionOnlineTextHandler';
|
||||||
|
type = 'onlinetext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the
|
||||||
|
* plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit
|
||||||
|
* unfiltered data.
|
||||||
|
*
|
||||||
|
* @return Boolean or promise resolved with boolean: whether it can be edited in offline.
|
||||||
|
*/
|
||||||
|
canEditOffline(): boolean {
|
||||||
|
// This plugin uses Moodle filters, it cannot be edited in offline.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a plugin has no data.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @return Whether the plugin is empty.
|
||||||
|
*/
|
||||||
|
isEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean {
|
||||||
|
const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true);
|
||||||
|
|
||||||
|
// If the text is empty, we can ignore files because they won't be visible anyways.
|
||||||
|
return text.trim().length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will be called when the user wants to create a new submission based on the previous one.
|
||||||
|
* It should add to pluginData the data to send to server based in the data in plugin (previous attempt).
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param pluginData Object where to store the data to send.
|
||||||
|
* @param userId User ID. If not defined, site's current user.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return If the function is async, it should return a Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async copySubmissionData(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
pluginData: AddonModAssignSubmissionOnlineTextPluginData,
|
||||||
|
userId?: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true);
|
||||||
|
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
|
||||||
|
let itemId = 0;
|
||||||
|
|
||||||
|
if (files.length) {
|
||||||
|
// Re-upload the files.
|
||||||
|
itemId = await AddonModAssignHelper.instance.uploadFiles(assign.id, files, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginData.onlinetext_editor = {
|
||||||
|
text: text,
|
||||||
|
format: 1,
|
||||||
|
itemid: itemId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Component to use to display the plugin data, either in read or in edit mode.
|
||||||
|
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||||
|
*
|
||||||
|
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(): Type<unknown> {
|
||||||
|
return AddonModAssignSubmissionOnlineTextComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get files used by this plugin.
|
||||||
|
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @return The files (or promise resolved with the files).
|
||||||
|
*/
|
||||||
|
getPluginFiles(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
): CoreWSExternalFile[] {
|
||||||
|
return AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @return The size (or promise resolved with size).
|
||||||
|
*/
|
||||||
|
async getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise<number> {
|
||||||
|
const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true);
|
||||||
|
const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin);
|
||||||
|
|
||||||
|
const filesSize = await CoreFileHelper.instance.getTotalFilesSize(files);
|
||||||
|
|
||||||
|
return text.length + filesSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param inputData Data entered by the user for the submission.
|
||||||
|
* @return The size (or promise resolved with size).
|
||||||
|
*/
|
||||||
|
getSizeForEdit(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
): number {
|
||||||
|
const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true);
|
||||||
|
|
||||||
|
return text.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the text to submit.
|
||||||
|
*
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param inputData Data entered by the user for the submission.
|
||||||
|
* @return Text to submit.
|
||||||
|
*/
|
||||||
|
protected getTextToSubmit(plugin: AddonModAssignPlugin, inputData: AddonModAssignSubmissionOnlineTextData): string {
|
||||||
|
const text = inputData.onlinetext_editor_text;
|
||||||
|
const files = plugin.fileareas && plugin.fileareas[0] && plugin.fileareas[0].files || [];
|
||||||
|
|
||||||
|
return CoreTextUtils.instance.restorePluginfileUrls(text, files || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the submission data has changed for this plugin.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param inputData Data entered by the user for the submission.
|
||||||
|
* @return Boolean (or promise resolved with boolean): whether the data has changed.
|
||||||
|
*/
|
||||||
|
async hasDataChanged(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
inputData: AddonModAssignSubmissionOnlineTextData,
|
||||||
|
): Promise<boolean> {
|
||||||
|
|
||||||
|
// Get the original text from plugin or offline.
|
||||||
|
const offlineData =
|
||||||
|
await CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getSubmission(assign.id, submission.userid));
|
||||||
|
|
||||||
|
let initialText = '';
|
||||||
|
if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) {
|
||||||
|
initialText = (<AddonModAssignSubmissionOnlineTextPluginData>offlineData.plugindata).onlinetext_editor.text;
|
||||||
|
} else {
|
||||||
|
// No offline data found, get text from plugin.
|
||||||
|
initialText = plugin.editorfields && plugin.editorfields[0] ? plugin.editorfields[0].text : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if text has changed.
|
||||||
|
return initialText != this.getTextToSubmit(plugin, inputData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled for edit on a site level.
|
||||||
|
*
|
||||||
|
* @return Whether or not the handler is enabled for edit on a site level.
|
||||||
|
*/
|
||||||
|
isEnabledForEdit(): boolean {
|
||||||
|
// There's a bug in Moodle 3.1.0 that doesn't allow submitting HTML, so we'll disable this plugin in that case.
|
||||||
|
// Bug was fixed in 3.1.1 minor release and in 3.2.
|
||||||
|
const currentSite = CoreSites.instance.getCurrentSite();
|
||||||
|
|
||||||
|
return !!currentSite?.isVersionGreaterEqualThan('3.1.1') || !!currentSite?.checkIfAppUsesLocalMobile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare and add to pluginData the data to send to the server based on the input data.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param inputData Data entered by the user for the submission.
|
||||||
|
* @param pluginData Object where to store the data to send.
|
||||||
|
* @param offline Whether the user is editing in offline.
|
||||||
|
* @param userId User ID. If not defined, site's current user.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return If the function is async, it should return a Promise resolved when done.
|
||||||
|
*/
|
||||||
|
prepareSubmissionData(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
inputData: AddonModAssignSubmissionOnlineTextData,
|
||||||
|
pluginData: AddonModAssignSubmissionOnlineTextPluginData,
|
||||||
|
): void | Promise<void> {
|
||||||
|
|
||||||
|
let text = this.getTextToSubmit(plugin, inputData);
|
||||||
|
|
||||||
|
// Check word limit.
|
||||||
|
const configs = AddonModAssignHelper.instance.getPluginConfig(assign, 'assignsubmission', plugin.type);
|
||||||
|
if (parseInt(configs.wordlimitenabled, 10)) {
|
||||||
|
const words = CoreTextUtils.instance.countWords(text);
|
||||||
|
const wordlimit = parseInt(configs.wordlimit, 10);
|
||||||
|
if (words > wordlimit) {
|
||||||
|
const params = { $a: { count: words, limit: wordlimit } };
|
||||||
|
const message = Translate.instance.instant('addon.mod_assign_submission_onlinetext.wordlimitexceeded', params);
|
||||||
|
|
||||||
|
throw new CoreError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some HTML to the text if needed.
|
||||||
|
text = CoreTextUtils.instance.formatHtmlLines(text);
|
||||||
|
|
||||||
|
pluginData.onlinetext_editor = {
|
||||||
|
text: text,
|
||||||
|
format: 1,
|
||||||
|
itemid: 0, // Can't add new files yet, so we use a fake itemid.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare and add to pluginData the data to send to the server based on the offline data stored.
|
||||||
|
* This will be used when performing a synchronization.
|
||||||
|
*
|
||||||
|
* @param assign The assignment.
|
||||||
|
* @param submission The submission.
|
||||||
|
* @param plugin The plugin object.
|
||||||
|
* @param offlineData Offline data stored.
|
||||||
|
* @param pluginData Object where to store the data to send.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return If the function is async, it should return a Promise resolved when done.
|
||||||
|
*/
|
||||||
|
prepareSyncData(
|
||||||
|
assign: AddonModAssignAssign,
|
||||||
|
submission: AddonModAssignSubmission,
|
||||||
|
plugin: AddonModAssignPlugin,
|
||||||
|
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
|
||||||
|
pluginData: AddonModAssignSubmissionOnlineTextPluginData,
|
||||||
|
): void | Promise<void> {
|
||||||
|
|
||||||
|
const offlinePluginData = <AddonModAssignSubmissionOnlineTextPluginData>(offlineData && offlineData.plugindata);
|
||||||
|
const textData = offlinePluginData.onlinetext_editor;
|
||||||
|
if (textData) {
|
||||||
|
// Has some data to sync.
|
||||||
|
pluginData.onlinetext_editor = textData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export const AddonModAssignSubmissionOnlineTextHandler = makeSingleton(AddonModAssignSubmissionOnlineTextHandlerService);
|
||||||
|
|
||||||
|
export type AddonModAssignSubmissionOnlineTextData = {
|
||||||
|
// The text for this submission.
|
||||||
|
onlinetext_editor_text: string; // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddonModAssignSubmissionOnlineTextPluginData = {
|
||||||
|
// Editor structure.
|
||||||
|
onlinetext_editor: { // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
text: string; // The text for this submission.
|
||||||
|
format: number; // The format for this submission.
|
||||||
|
itemid: number; // The draft area id for files attached to the submission.
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { AddonModAssignSubmissionCommentsModule } from './comments/comments.module';
|
||||||
|
import { AddonModAssignSubmissionFileModule } from './file/file.module';
|
||||||
|
import { AddonModAssignSubmissionOnlineTextModule } from './onlinetext/onlinetext.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
AddonModAssignSubmissionCommentsModule,
|
||||||
|
AddonModAssignSubmissionFileModule,
|
||||||
|
AddonModAssignSubmissionOnlineTextModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModAssignSubmissionModule { }
|
|
@ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: ':courseId/:cmdId',
|
path: ':courseId/:cmId',
|
||||||
loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModBookIndexPageModule),
|
loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModBookIndexPageModule),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
(action)="expandDescription()" iconAction="fas-arrow-right">
|
(action)="expandDescription()" iconAction="fas-arrow-right">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
|
<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>
|
||||||
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
|
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
|
||||||
(action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
|
(action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: ':courseId/:cmdId',
|
path: ':courseId/:cmId',
|
||||||
loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModLessonIndexPageModule),
|
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> {
|
protected async prefetchLesson(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean): Promise<void> {
|
||||||
const siteId = CoreSites.instance.getCurrentSiteId();
|
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
courseId = courseId || module.course || 1;
|
courseId = courseId || module.course || CoreSites.instance.getCurrentSiteHomeId();
|
||||||
|
|
||||||
const commonOptions = {
|
const commonOptions = {
|
||||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
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 { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
|
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
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.
|
* @param force Wether to force sync not depending on last execution.
|
||||||
* @return Promise resolved if sync is successful, rejected if sync fails.
|
* @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);
|
return this.syncOnSites('all lessons', this.syncAllLessonsFunc.bind(this, !!force), siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
|
||||||
*/
|
*/
|
||||||
async syncLessonIfNeeded(
|
async syncLessonIfNeeded(
|
||||||
lessonId: number,
|
lessonId: number,
|
||||||
askPassword?: boolean,
|
askPassword = false,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<AddonModLessonSyncResult | undefined> {
|
): Promise<AddonModLessonSyncResult | undefined> {
|
||||||
const needed = await this.isSyncNeeded(lessonId, siteId);
|
const needed = await this.isSyncNeeded(lessonId, siteId);
|
||||||
|
@ -184,8 +184,8 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
|
||||||
*/
|
*/
|
||||||
async syncLesson(
|
async syncLesson(
|
||||||
lessonId: number,
|
lessonId: number,
|
||||||
askPassword?: boolean,
|
askPassword = false,
|
||||||
ignoreBlock?: boolean,
|
ignoreBlock = false,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<AddonModLessonSyncResult> {
|
): Promise<AddonModLessonSyncResult> {
|
||||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
@ -201,7 +201,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
|
||||||
if (!ignoreBlock && CoreSync.instance.isBlocked(AddonModLessonProvider.COMPONENT, lessonId, siteId)) {
|
if (!ignoreBlock && CoreSync.instance.isBlocked(AddonModLessonProvider.COMPONENT, lessonId, siteId)) {
|
||||||
this.logger.debug('Cannot sync lesson ' + lessonId + ' because it is blocked.');
|
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);
|
this.logger.debug('Try to sync lesson ' + lessonId + ' in site ' + siteId);
|
||||||
|
@ -222,8 +222,8 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
|
||||||
*/
|
*/
|
||||||
protected async performSyncLesson(
|
protected async performSyncLesson(
|
||||||
lessonId: number,
|
lessonId: number,
|
||||||
askPassword?: boolean,
|
askPassword = false,
|
||||||
ignoreBlock?: boolean,
|
ignoreBlock = false,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<AddonModLessonSyncResult> {
|
): Promise<AddonModLessonSyncResult> {
|
||||||
// Sync offline logs.
|
// Sync offline logs.
|
||||||
|
@ -270,7 +270,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
|
||||||
protected async syncAttempts(
|
protected async syncAttempts(
|
||||||
lessonId: number,
|
lessonId: number,
|
||||||
result: AddonModLessonSyncResult,
|
result: AddonModLessonSyncResult,
|
||||||
askPassword?: boolean,
|
askPassword = false,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<AddonModLessonGetPasswordResult | undefined> {
|
): Promise<AddonModLessonGetPasswordResult | undefined> {
|
||||||
let attempts = await AddonModLessonOffline.instance.getLessonAttempts(lessonId, siteId);
|
let attempts = await AddonModLessonOffline.instance.getLessonAttempts(lessonId, siteId);
|
||||||
|
@ -408,8 +408,8 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
|
||||||
lessonId: number,
|
lessonId: number,
|
||||||
result: AddonModLessonSyncResult,
|
result: AddonModLessonSyncResult,
|
||||||
passwordData?: AddonModLessonGetPasswordResult,
|
passwordData?: AddonModLessonGetPasswordResult,
|
||||||
askPassword?: boolean,
|
askPassword = false,
|
||||||
ignoreBlock?: boolean,
|
ignoreBlock = false,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Attempts sent or there was none. If there is a finished retake, send it.
|
// Attempts sent or there was none. If there is a finished retake, send it.
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { AddonModAssignModule } from './assign/assign.module';
|
||||||
import { AddonModBookModule } from './book/book.module';
|
import { AddonModBookModule } from './book/book.module';
|
||||||
import { AddonModLessonModule } from './lesson/lesson.module';
|
import { AddonModLessonModule } from './lesson/lesson.module';
|
||||||
import { AddonModPageModule } from './page/page.module';
|
import { AddonModPageModule } from './page/page.module';
|
||||||
|
@ -21,6 +22,7 @@ import { AddonModPageModule } from './page/page.module';
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [],
|
declarations: [],
|
||||||
imports: [
|
imports: [
|
||||||
|
AddonModAssignModule,
|
||||||
AddonModBookModule,
|
AddonModBookModule,
|
||||||
AddonModLessonModule,
|
AddonModLessonModule,
|
||||||
AddonModPageModule,
|
AddonModPageModule,
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: ':courseId/:cmdId',
|
path: ':courseId/:cmId',
|
||||||
loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModPageIndexPageModule),
|
loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModPageIndexPageModule),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 341 B |
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.
|
* @param time Time to set. If not defined, current time.
|
||||||
* @return Promise resolved when the time is set.
|
* @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();
|
time = typeof time != 'undefined' ? time : Date.now();
|
||||||
|
|
||||||
await CoreSync.instance.insertOrUpdateSyncRecord(this.component, id, { time: time }, siteId);
|
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.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved when done.
|
* @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 || []);
|
const warningsText = JSON.stringify(warnings || []);
|
||||||
|
|
||||||
await CoreSync.instance.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId);
|
await CoreSync.instance.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId);
|
||||||
|
|
|
@ -42,7 +42,7 @@ export class CoreIonLoadingElement {
|
||||||
* Present the loading.
|
* Present the loading.
|
||||||
*/
|
*/
|
||||||
async present(): Promise<void> {
|
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);
|
await CoreUtils.instance.wait(40);
|
||||||
|
|
||||||
if (!this.isDismissed) {
|
if (!this.isDismissed) {
|
||||||
|
|
|
@ -55,8 +55,12 @@ export abstract class CorePageItemsListManager<Item> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process page started operations.
|
* 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.
|
// Calculate current selected item.
|
||||||
const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent });
|
const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent });
|
||||||
if (route !== null && route.firstChild) {
|
if (route !== null && route.firstChild) {
|
||||||
|
|
|
@ -71,6 +71,7 @@ export class CoreFaIconDirective implements OnChanges {
|
||||||
if (library != 'ionic') {
|
if (library != 'ionic') {
|
||||||
const src = `assets/fonts/font-awesome/${library}/${iconName}.svg`;
|
const src = `assets/fonts/font-awesome/${library}/${iconName}.svg`;
|
||||||
this.element.setAttribute('src', src);
|
this.element.setAttribute('src', src);
|
||||||
|
this.element.classList.add('faicon');
|
||||||
|
|
||||||
if (CoreConstants.BUILD.isDevelopment || CoreConstants.BUILD.isTesting) {
|
if (CoreConstants.BUILD.isDevelopment || CoreConstants.BUILD.isTesting) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler';
|
||||||
/**
|
/**
|
||||||
* Base class to create activity sync providers. It provides some common functions.
|
* 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.
|
* Conveniece function to prefetch data after an update.
|
||||||
|
|
|
@ -522,7 +522,14 @@ export class CoreCourseProvider {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved with the module's grade info.
|
* @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 info = await this.getModuleBasicInfo(moduleId, siteId);
|
||||||
|
|
||||||
const grade: CoreCourseModuleGradeInfo = {
|
const grade: CoreCourseModuleGradeInfo = {
|
||||||
|
@ -539,10 +546,11 @@ export class CoreCourseProvider {
|
||||||
typeof grade.advancedgrading != 'undefined' ||
|
typeof grade.advancedgrading != 'undefined' ||
|
||||||
typeof grade.outcomes != 'undefined'
|
typeof grade.outcomes != 'undefined'
|
||||||
) {
|
) {
|
||||||
|
// On 3.1 won't get grading info and will return undefined.
|
||||||
return grade;
|
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 = {
|
export type CoreCourseModuleGradeInfo = {
|
||||||
grade?: number; // Grade (max value or scale id).
|
grade?: number; // Grade (max value or scale id).
|
||||||
scale?: string; // Scale items (if used).
|
scale?: string; // Scale items (if used).
|
||||||
gradepass?: string; // Grade to pass (float).
|
gradepass?: string; // Grade to pass (float).
|
||||||
gradecat?: number; // Grade category.
|
gradecat?: number; // Grade category.
|
||||||
advancedgrading?: { // Advanced grading settings.
|
advancedgrading?: CoreCourseModuleAdvancedGradingSetting[]; // Advanced grading settings.
|
||||||
|
outcomes?: CoreCourseModuleGradeOutcome[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced grading settings.
|
||||||
|
*/
|
||||||
|
export type CoreCourseModuleAdvancedGradingSetting = {
|
||||||
area: string; // Gradable area name.
|
area: string; // Gradable area name.
|
||||||
method: string; // Grading method.
|
method: string; // Grading method.
|
||||||
}[];
|
};
|
||||||
outcomes?: { // Outcomes information.
|
|
||||||
|
/**
|
||||||
|
* Grade outcome information.
|
||||||
|
*/
|
||||||
|
export type CoreCourseModuleGradeOutcome = {
|
||||||
id: string; // Outcome id.
|
id: string; // Outcome id.
|
||||||
name: string; // Outcome full name.
|
name: string; // Outcome full name.
|
||||||
scale: string; // Scale items.
|
scale: string; // Scale items.
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import "~theme/globals";
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
.course-icon {
|
.course-icon {
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -10,35 +12,10 @@
|
||||||
transition: all 50ms ease-in-out;
|
transition: all 50ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-icon[course-color="0"] {
|
@for $i from 0 to length($core-course-image-background) {
|
||||||
color: var(--core-course-color-0);
|
ion-icon[course-color="#{$i}"] {
|
||||||
|
color: nth($core-course-image-background, $i + 1);
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-avatar {
|
ion-avatar {
|
||||||
|
|
|
@ -7,35 +7,10 @@
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
height: calc(100% - 20px);
|
height: calc(100% - 20px);
|
||||||
|
|
||||||
&[course-color="0"] .core-course-thumb {
|
@for $i from 0 to length($core-course-image-background) {
|
||||||
background: var(--core-course-color-0);
|
&[course-color="#{$i}"] .core-course-thumb {
|
||||||
|
background: nth($core-course-image-background, $i + 1);
|
||||||
}
|
}
|
||||||
&[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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-course-thumb {
|
.core-course-thumb {
|
||||||
|
|
|
@ -63,8 +63,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
||||||
async ngAfterViewInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
await this.fetchInitialGrades();
|
await this.fetchInitialGrades();
|
||||||
|
|
||||||
this.grades.watchSplitViewOutlet(this.splitView);
|
this.grades.start(this.splitView);
|
||||||
this.grades.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -42,8 +42,7 @@ export class CoreGradesCoursesPage implements OnDestroy, AfterViewInit {
|
||||||
async ngAfterViewInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
await this.fetchInitialCourses();
|
await this.fetchInitialCourses();
|
||||||
|
|
||||||
this.courses.watchSplitViewOutlet(this.splitView);
|
this.courses.start(this.splitView);
|
||||||
this.courses.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { CoreMenuItem, CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that provides some features regarding grades information.
|
* Service that provides some features regarding grades information.
|
||||||
|
@ -51,16 +52,18 @@ export class CoreGradesHelperProvider {
|
||||||
* @return Formatted row object.
|
* @return Formatted row object.
|
||||||
*/
|
*/
|
||||||
protected formatGradeRow(tableRow: CoreGradesTableRow): CoreGradesFormattedRow {
|
protected formatGradeRow(tableRow: CoreGradesTableRow): CoreGradesFormattedRow {
|
||||||
const row = {};
|
const row: CoreGradesFormattedRow = {
|
||||||
|
rowclass: '',
|
||||||
|
};
|
||||||
for (const name in tableRow) {
|
for (const name in tableRow) {
|
||||||
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
|
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
|
||||||
let content = String(tableRow[name].content);
|
let content = String(tableRow[name].content);
|
||||||
|
|
||||||
if (name == 'itemname') {
|
if (name == 'itemname') {
|
||||||
this.setRowIcon(row, content);
|
this.setRowIcon(row, content);
|
||||||
row['link'] = this.getModuleLink(content);
|
row.link = this.getModuleLink(content);
|
||||||
row['rowclass'] += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : '';
|
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('dimmed_text') >= 0 ? ' dimmed_text' : '';
|
||||||
|
|
||||||
content = content.replace(/<\/span>/gi, '\n');
|
content = content.replace(/<\/span>/gi, '\n');
|
||||||
content = CoreTextUtils.instance.cleanTags(content);
|
content = CoreTextUtils.instance.cleanTags(content);
|
||||||
|
@ -86,20 +89,20 @@ export class CoreGradesHelperProvider {
|
||||||
* @return Formatted row object.
|
* @return Formatted row object.
|
||||||
*/
|
*/
|
||||||
protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable {
|
protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable {
|
||||||
const row = {};
|
const row: CoreGradesFormattedRowForTable = {};
|
||||||
for (let name in tableRow) {
|
for (let name in tableRow) {
|
||||||
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
|
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
|
||||||
let content = String(tableRow[name].content);
|
let content = String(tableRow[name].content);
|
||||||
|
|
||||||
if (name == 'itemname') {
|
if (name == 'itemname') {
|
||||||
row['id'] = parseInt(tableRow[name]!.id.split('_')[1], 10);
|
row.id = parseInt(tableRow[name]!.id.split('_')[1], 10);
|
||||||
row['colspan'] = tableRow[name]!.colspan;
|
row.colspan = tableRow[name]!.colspan;
|
||||||
row['rowspan'] = (tableRow['leader'] && tableRow['leader'].rowspan) || 1;
|
row.rowspan = (tableRow.leader && tableRow.leader.rowspan) || 1;
|
||||||
|
|
||||||
this.setRowIcon(row, content);
|
this.setRowIcon(row, content);
|
||||||
row['rowclass'] = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even';
|
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('hidden') >= 0 ? ' hidden' : '';
|
||||||
row['rowclass'] += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
|
row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
|
||||||
|
|
||||||
content = content.replace(/<\/span>/gi, '\n');
|
content = content.replace(/<\/span>/gi, '\n');
|
||||||
content = CoreTextUtils.instance.cleanTags(content);
|
content = CoreTextUtils.instance.cleanTags(content);
|
||||||
|
@ -202,14 +205,14 @@ export class CoreGradesHelperProvider {
|
||||||
*/
|
*/
|
||||||
async getGradesCourseData(grades: CoreGradesGradeOverview[]): Promise<CoreGradesGradeOverviewWithCourseData[]> {
|
async getGradesCourseData(grades: CoreGradesGradeOverview[]): Promise<CoreGradesGradeOverviewWithCourseData[]> {
|
||||||
// Obtain courses from cache to prevent network requests.
|
// Obtain courses from cache to prevent network requests.
|
||||||
let coursesWereMissing;
|
let coursesWereMissing = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const courses = await CoreCourses.instance.getUserCourses(undefined, undefined, CoreSitesReadingStrategy.OnlyCache);
|
const courses = await CoreCourses.instance.getUserCourses(undefined, undefined, CoreSitesReadingStrategy.OnlyCache);
|
||||||
const coursesMap = CoreUtils.instance.arrayToObject(courses, 'id');
|
const coursesMap = CoreUtils.instance.arrayToObject(courses, 'id');
|
||||||
|
|
||||||
coursesWereMissing = this.addCourseData(grades, coursesMap);
|
coursesWereMissing = this.addCourseData(grades, coursesMap);
|
||||||
} catch (error) {
|
} catch {
|
||||||
coursesWereMissing = true;
|
coursesWereMissing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,7 +281,7 @@ export class CoreGradesHelperProvider {
|
||||||
const grades = await CoreGrades.instance.getCourseGradesTable(courseId, userId, siteId, ignoreCache);
|
const grades = await CoreGrades.instance.getCourseGradesTable(courseId, userId, siteId, ignoreCache);
|
||||||
|
|
||||||
if (!grades) {
|
if (!grades) {
|
||||||
throw new Error('Couldn\'t get grade item');
|
throw new CoreError('Couldn\'t get grade item');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getGradesTableRow(grades, gradeId);
|
return this.getGradesTableRow(grades, gradeId);
|
||||||
|
@ -325,15 +328,15 @@ export class CoreGradesHelperProvider {
|
||||||
groupId?: number,
|
groupId?: number,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
ignoreCache: boolean = false,
|
ignoreCache: boolean = false,
|
||||||
): Promise<CoreGradesFormattedItem> {
|
): Promise<CoreGradesFormattedItem[] | CoreGradesFormattedRow[]> {
|
||||||
const grades = await CoreGrades.instance.getGradeItems(courseId, userId, groupId, siteId, ignoreCache);
|
const grades = await CoreGrades.instance.getGradeItems(courseId, userId, groupId, siteId, ignoreCache);
|
||||||
|
|
||||||
if (!grades) {
|
if (!grades) {
|
||||||
throw new Error('Couldn\'t get grade module items');
|
throw new CoreError('Couldn\'t get grade module items');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('tabledata' in grades) {
|
if ('tabledata' in grades) {
|
||||||
// Table format.
|
// 3.1 Table format.
|
||||||
return this.getModuleGradesTableRows(grades, moduleId);
|
return this.getModuleGradesTableRows(grades, moduleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,18 +350,16 @@ export class CoreGradesHelperProvider {
|
||||||
* @param selectedGrade Selected grade label.
|
* @param selectedGrade Selected grade label.
|
||||||
* @return Selected grade value.
|
* @return Selected grade value.
|
||||||
*/
|
*/
|
||||||
getGradeValueFromLabel(grades: CoreMenuItem[], selectedGrade: string): number {
|
getGradeValueFromLabel(grades: CoreMenuItem[], selectedGrade?: string): number {
|
||||||
if (!grades || !selectedGrade) {
|
if (!grades || !selectedGrade) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const x in grades) {
|
const grade = grades.find((grade) => grade.label == selectedGrade);
|
||||||
if (grades[x].label == selectedGrade) {
|
|
||||||
return grades[x].value < 0 ? 0 : grades[x].value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return !grade || grade.value < 0
|
||||||
|
? 0
|
||||||
|
: grade.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -457,15 +458,15 @@ export class CoreGradesHelperProvider {
|
||||||
siteId = site.id;
|
siteId = site.id;
|
||||||
currentUserId = site.getUserId();
|
currentUserId = site.getUserId();
|
||||||
|
|
||||||
if (moduleId) {
|
if (!moduleId) {
|
||||||
|
throw new CoreError('Invalid moduleId');
|
||||||
|
}
|
||||||
|
|
||||||
// Try to open the module grade directly. Check if it's possible.
|
// Try to open the module grade directly. Check if it's possible.
|
||||||
const grades = await CoreGrades.instance.isGradeItemsAvalaible(siteId);
|
const grades = await CoreGrades.instance.isGradeItemsAvalaible(siteId);
|
||||||
|
|
||||||
if (!grades) {
|
if (!grades) {
|
||||||
throw new Error();
|
throw new CoreError('No grades found.');
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -476,7 +477,7 @@ export class CoreGradesHelperProvider {
|
||||||
const item = Array.isArray(items) && items.find((item) => moduleId == item.cmid);
|
const item = Array.isArray(items) && items.find((item) => moduleId == item.cmid);
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error();
|
throw new CoreError('Grade item not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the item directly.
|
// Open the item directly.
|
||||||
|
@ -560,46 +561,49 @@ export class CoreGradesHelperProvider {
|
||||||
* @param text HTML where the image will be rendered.
|
* @param text HTML where the image will be rendered.
|
||||||
* @return Row object with the image.
|
* @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', '/');
|
text = text.replace('%2F', '/').replace('%2f', '/');
|
||||||
|
|
||||||
if (text.indexOf('/agg_mean') > -1) {
|
if (text.indexOf('/agg_mean') > -1) {
|
||||||
row['itemtype'] = 'agg_mean';
|
row.itemtype = 'agg_mean';
|
||||||
row['image'] = 'assets/img/grades/agg_mean.png';
|
row.image = 'assets/img/grades/agg_mean.png';
|
||||||
} else if (text.indexOf('/agg_sum') > -1) {
|
} else if (text.indexOf('/agg_sum') > -1) {
|
||||||
row['itemtype'] = 'agg_sum';
|
row.itemtype = 'agg_sum';
|
||||||
row['image'] = 'assets/img/grades/agg_sum.png';
|
row.image = 'assets/img/grades/agg_sum.png';
|
||||||
} else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks') > -1) {
|
} else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks') > -1) {
|
||||||
row['itemtype'] = 'outcome';
|
row.itemtype = 'outcome';
|
||||||
row['icon'] = 'fa-tasks';
|
row.icon = 'fas-chart-pie';
|
||||||
} else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder') > -1) {
|
} else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder') > -1) {
|
||||||
row['itemtype'] = 'category';
|
row.itemtype = 'category';
|
||||||
row['icon'] = 'fa-folder';
|
row.icon = 'fas-cubes';
|
||||||
} else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o') > -1) {
|
} else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o') > -1) {
|
||||||
row['itemtype'] = 'manual';
|
row.itemtype = 'manual';
|
||||||
row['icon'] = 'fa-square-o';
|
row.icon = 'far-square';
|
||||||
} else if (text.indexOf('/mod/') > -1) {
|
} else if (text.indexOf('/mod/') > -1) {
|
||||||
const module = text.match(/mod\/([^/]*)\//);
|
const module = text.match(/mod\/([^/]*)\//);
|
||||||
if (typeof module?.[1] != 'undefined') {
|
if (typeof module?.[1] != 'undefined') {
|
||||||
row['itemtype'] = 'mod';
|
row.itemtype = 'mod';
|
||||||
row['itemmodule'] = module[1];
|
row.itemmodule = module[1];
|
||||||
row['image'] = CoreCourse.instance.getModuleIconSrc(
|
row.image = CoreCourse.instance.getModuleIconSrc(
|
||||||
module[1],
|
module[1],
|
||||||
CoreDomUtils.instance.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined,
|
CoreDomUtils.instance.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (row['rowspan'] && row['rowspan'] > 1) {
|
if (row.rowspan && row.rowspan > 1) {
|
||||||
row['itemtype'] = 'category';
|
row.itemtype = 'category';
|
||||||
row['icon'] = 'fa-folder';
|
row.icon = 'fas-cubes';
|
||||||
} else if (text.indexOf('src=') > -1) {
|
} else if (text.indexOf('src=') > -1) {
|
||||||
row['itemtype'] = 'unknown';
|
row.itemtype = 'unknown';
|
||||||
const src = text.match(/src="([^"]*)"/);
|
const src = text.match(/src="([^"]*)"/);
|
||||||
row['image'] = src?.[1];
|
row.image = src?.[1];
|
||||||
} else if (text.indexOf('<i ') > -1) {
|
} else if (text.indexOf('<i ') > -1) {
|
||||||
row['itemtype'] = 'unknown';
|
row.itemtype = 'unknown';
|
||||||
const src = text.match(/<i class="(?:[^"]*?\s)?(fa-[a-z0-9-]+)/);
|
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([]);
|
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) {}
|
export class CoreGradesHelper extends makeSingleton(CoreGradesHelperProvider) {}
|
||||||
|
|
||||||
// @todo formatted data types.
|
// @todo formatted data types.
|
||||||
export type CoreGradesFormattedRow = any;
|
|
||||||
export type CoreGradesFormattedRowForTable = any;
|
export type CoreGradesFormattedRowForTable = any;
|
||||||
export type CoreGradesFormattedItem = any;
|
|
||||||
export type CoreGradesFormattedTableColumn = 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 CoreGradesFormattedTableRow = CoreGradesFormattedTableRowFilled | CoreGradesFormattedTableRowEmpty;
|
||||||
export type CoreGradesFormattedTable = {
|
export type CoreGradesFormattedTable = {
|
||||||
columns: CoreGradesFormattedTableColumn[];
|
columns: CoreGradesFormattedTableColumn[];
|
||||||
|
|
|
@ -339,8 +339,10 @@ export class CoreGradesProvider {
|
||||||
* @return True if ws is avalaible, false otherwise.
|
* @return True if ws is avalaible, false otherwise.
|
||||||
* @since Moodle 3.2
|
* @since Moodle 3.2
|
||||||
*/
|
*/
|
||||||
isGradeItemsAvalaible(siteId?: string): Promise<boolean> {
|
async isGradeItemsAvalaible(siteId?: string): Promise<boolean> {
|
||||||
return CoreSites.instance.getSite(siteId).then((site) => site.wsAvailable('gradereport_user_get_grade_items'));
|
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 {
|
ngAfterViewInit(): void {
|
||||||
this.sections.setItems(CoreSettingsConstants.SECTIONS);
|
this.sections.setItems(CoreSettingsConstants.SECTIONS);
|
||||||
this.sections.watchSplitViewOutlet(this.splitView);
|
this.sections.start(this.splitView);
|
||||||
this.sections.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -62,8 +62,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
||||||
async ngAfterViewInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
await this.fetchInitialParticipants();
|
await this.fetchInitialParticipants();
|
||||||
|
|
||||||
this.participants.watchSplitViewOutlet(this.splitView);
|
this.participants.start(this.splitView);
|
||||||
this.participants.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -267,7 +267,7 @@ export class CoreCronDelegateService {
|
||||||
* @return True if handler uses network or not defined, false otherwise.
|
* @return True if handler uses network or not defined, false otherwise.
|
||||||
*/
|
*/
|
||||||
protected handlerUsesNetwork(name: string): boolean {
|
protected handlerUsesNetwork(name: string): boolean {
|
||||||
if (!this.handlers[name] || this.handlers[name].usesNetwork) {
|
if (!this.handlers[name] || !this.handlers[name].usesNetwork) {
|
||||||
// Invalid, return default.
|
// Invalid, return default.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -412,7 +412,7 @@ export class CoreGroupsProvider {
|
||||||
* @param groupInfo Group info.
|
* @param groupInfo Group info.
|
||||||
* @return Group ID to use.
|
* @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) {
|
if (groupId > 0 && groupInfo && groupInfo.groups && groupInfo.groups.length > 0) {
|
||||||
// Check if the group is in the list of groups.
|
// Check if the group is in the list of groups.
|
||||||
if (groupInfo.groups.some((group) => groupId == group.id)) {
|
if (groupInfo.groups.some((group) => groupId == group.id)) {
|
||||||
|
|
|
@ -243,12 +243,34 @@ export class CoreNavigatorService {
|
||||||
return CoreUrlUtils.instance.removeUrlParams(this.previousPath || '');
|
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.
|
* 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,
|
* 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.
|
* unless there's a new navigation to the page.
|
||||||
*
|
*
|
||||||
* @param name Name of the parameter.
|
* @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.
|
* @return Value of the parameter, undefined if not found.
|
||||||
*/
|
*/
|
||||||
getRouteParam<T = unknown>(name: string, params?: Params): T | undefined {
|
getRouteParam<T = unknown>(name: string, params?: Params): T | undefined {
|
||||||
|
@ -256,15 +278,16 @@ export class CoreNavigatorService {
|
||||||
|
|
||||||
if (!params) {
|
if (!params) {
|
||||||
const route = this.getCurrentRoute();
|
const route = this.getCurrentRoute();
|
||||||
if (!route.snapshot) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = route.snapshot.queryParams[name] ?? route.snapshot.params[name];
|
value = this.getRouteSnapshotParam(name, route);
|
||||||
} else {
|
} else {
|
||||||
value = params[name];
|
value = params[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof value == 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let storedParam = this.storedParams[value];
|
let storedParam = this.storedParams[value];
|
||||||
|
|
||||||
// Remove the parameter from our map if it's in there.
|
// 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.
|
* Angular router automatically converts numbers to string, this function automatically converts it back to number.
|
||||||
*
|
*
|
||||||
* @param name Name of the parameter.
|
* @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.
|
* @return Value of the parameter, undefined if not found.
|
||||||
*/
|
*/
|
||||||
getRouteNumberParam(name: string, params?: Params): number | undefined {
|
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.
|
* Angular router automatically converts booleans to string, this function automatically converts it back to boolean.
|
||||||
*
|
*
|
||||||
* @param name Name of the parameter.
|
* @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.
|
* @return Value of the parameter, undefined if not found.
|
||||||
*/
|
*/
|
||||||
getRouteBooleanParam(name: string, params?: Params): boolean | undefined {
|
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.
|
// IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
|
||||||
// @todo this.location.replaceState('');
|
// @todo this.location.replaceState('');
|
||||||
|
|
||||||
|
options = {
|
||||||
|
preferCurrentTab: true,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
path = path.replace(/^(\.|\/main)?\//, '');
|
path = path.replace(/^(\.|\/main)?\//, '');
|
||||||
|
|
||||||
const pathRoot = /^[^/]+/.exec(path)?.[0] ?? '';
|
const pathRoot = /^[^/]+/.exec(path)?.[0] ?? '';
|
||||||
|
@ -364,7 +394,7 @@ export class CoreNavigatorService {
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (options.preferCurrentTab === false && isMainMenuTab) {
|
if (!options.preferCurrentTab && isMainMenuTab) {
|
||||||
return this.navigate(`/main/${path}`, options);
|
return this.navigate(`/main/${path}`, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue