commit
						fc39c3e30e
					
				| @ -51,8 +51,7 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy { | ||||
|     async ngAfterViewInit(): Promise<void> { | ||||
|         await this.fetchInitialBadges(); | ||||
| 
 | ||||
|         this.badges.watchSplitViewOutlet(this.splitView); | ||||
|         this.badges.start(); | ||||
|         this.badges.start(this.splitView); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -19,11 +19,13 @@ import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreEditorComponentsModule } from '@features/editor/components/components.module'; | ||||
| 
 | ||||
| import { AddonCalendarEditEventPage } from './edit-event.page'; | ||||
| import { CanLeaveGuard } from '@guards/can-leave'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: AddonCalendarEditEventPage, | ||||
|         canDeactivate: [CanLeaveGuard], | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreSyncBaseProvider } from '@classes/base-sync'; | ||||
| import { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| @ -27,9 +27,9 @@ import { | ||||
| import { AddonCalendarOffline } from './calendar-offline'; | ||||
| import { AddonCalendarHelper } from './calendar-helper'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreNetworkError } from '@classes/errors/network-error'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to sync calendar. | ||||
| @ -52,21 +52,23 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     async syncAllEvents(siteId?: string, force?: boolean): Promise<void> { | ||||
|     async syncAllEvents(siteId?: string, force = false): Promise<void> { | ||||
|         await this.syncOnSites('all calendar events', this.syncAllEventsFunc.bind(this, [force]), siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync all events on a site. | ||||
|      * | ||||
|      * @param siteId Site ID to sync. | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @param siteId Site ID to sync. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     protected async syncAllEventsFunc(siteId: string, force?: boolean): Promise<void> { | ||||
|         const result = await (force ? this.syncEvents(siteId) : this.syncEventsIfNeeded(siteId)); | ||||
|     protected async syncAllEventsFunc(force = false, siteId?: string): Promise<void> { | ||||
|         const result = force | ||||
|             ? await this.syncEvents(siteId) | ||||
|             : await this.syncEventsIfNeeded(siteId); | ||||
| 
 | ||||
|         if (result && result.updated) { | ||||
|         if (result?.updated) { | ||||
|             // Sync successful, send event.
 | ||||
|             CoreEvents.trigger<AddonCalendarSyncEvents>(AddonCalendarSyncProvider.AUTO_SYNCED, result, siteId); | ||||
|         } | ||||
| @ -78,13 +80,13 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when the events are synced or if it doesn't need to be synced. | ||||
|      */ | ||||
|     async syncEventsIfNeeded(siteId?: string): Promise<void> { | ||||
|     async syncEventsIfNeeded(siteId?: string): Promise<AddonCalendarSyncEvents | undefined> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const needed = await this.isSyncNeeded(AddonCalendarSyncProvider.SYNC_ID, siteId); | ||||
| 
 | ||||
|         if (needed) { | ||||
|             await this.syncEvents(siteId); | ||||
|             return this.syncEvents(siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -125,17 +127,12 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda | ||||
|             updated: false, | ||||
|         }; | ||||
| 
 | ||||
|         let eventIds: number[] = []; | ||||
|         try { | ||||
|             eventIds = await AddonCalendarOffline.instance.getAllEventsIds(siteId); | ||||
|         } catch { | ||||
|             // No offline data found.
 | ||||
|         } | ||||
|         const eventIds: number[] = await CoreUtils.instance.ignoreErrors(AddonCalendarOffline.instance.getAllEventsIds(siteId), []); | ||||
| 
 | ||||
|         if (eventIds.length > 0) { | ||||
|             if (!CoreApp.instance.isOnline()) { | ||||
|                 // Cannot sync in offline.
 | ||||
|                 throw new CoreError('Cannot sync while offline'); | ||||
|                 throw new CoreNetworkError(); | ||||
|             } | ||||
| 
 | ||||
|             const promises = eventIds.map((eventId) => this.syncOfflineEvent(eventId, result, siteId)); | ||||
| @ -175,10 +172,10 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda | ||||
|         if (CoreSync.instance.isBlocked(AddonCalendarProvider.COMPONENT, eventId, siteId)) { | ||||
|             this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.'); | ||||
| 
 | ||||
|             throw Translate.instance.instant( | ||||
|             throw new CoreSyncBlockedError(Translate.instance.instant( | ||||
|                 'core.errorsyncblocked', | ||||
|                 { $a: Translate.instance.instant('addon.calendar.calendarevent') }, | ||||
|             ); | ||||
|             )); | ||||
|         } | ||||
| 
 | ||||
|         // First of all, check if the event has been deleted.
 | ||||
|  | ||||
| @ -34,6 +34,7 @@ import { CoreUserDelegate } from '@features/user/services/user-delegate'; | ||||
| import { AddonMessagesSendMessageUserHandler } from './services/handlers/user-send-message'; | ||||
| import { Network, NgZone } from '@singletons'; | ||||
| import { AddonMessagesSync } from './services/messages-sync'; | ||||
| import { AddonMessagesSyncCronHandler } from './services/handlers/sync-cron'; | ||||
| 
 | ||||
| const mainMenuChildrenRoutes: Routes = [ | ||||
|     { | ||||
| @ -61,7 +62,7 @@ const mainMenuChildrenRoutes: Routes = [ | ||||
|                 // Register handlers.
 | ||||
|                 CoreMainMenuDelegate.instance.registerHandler(AddonMessagesMainMenuHandler.instance); | ||||
|                 CoreCronDelegate.instance.register(AddonMessagesMainMenuHandler.instance); | ||||
|                 CoreCronDelegate.instance.register(AddonMessagesPushClickHandler.instance); | ||||
|                 CoreCronDelegate.instance.register(AddonMessagesSyncCronHandler.instance); | ||||
|                 CoreSettingsDelegate.instance.registerHandler(AddonMessagesSettingsHandler.instance); | ||||
|                 CoreContentLinksDelegate.instance.registerHandler(AddonMessagesIndexLinkHandler.instance); | ||||
|                 CoreContentLinksDelegate.instance.registerHandler(AddonMessagesDiscussionLinkHandler.instance); | ||||
|  | ||||
							
								
								
									
										85
									
								
								src/addons/mod/assign/assign-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/addons/mod/assign/assign-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { conditionalRoutes } from '@/app/app-routing.module'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { CanLeaveGuard } from '@guards/can-leave'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { AddonModAssignComponentsModule } from './components/components.module'; | ||||
| import { AddonModAssignEditPage } from './pages/edit/edit'; | ||||
| import { AddonModAssignIndexPage } from './pages/index/index.page'; | ||||
| import { AddonModAssignSubmissionListPage } from './pages/submission-list/submission-list.page'; | ||||
| import { AddonModAssignSubmissionReviewPage } from './pages/submission-review/submission-review'; | ||||
| 
 | ||||
| const commonRoutes: Routes = [ | ||||
|     { | ||||
|         path: ':courseId/:cmId', | ||||
|         component: AddonModAssignIndexPage, | ||||
|     }, | ||||
|     { | ||||
|         path: ':courseId/:cmId/edit', | ||||
|         component: AddonModAssignEditPage, | ||||
|         canDeactivate: [CanLeaveGuard], | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| const mobileRoutes: Routes = [ | ||||
|     ...commonRoutes, | ||||
|     { | ||||
|         path: ':courseId/:cmId/submission', | ||||
|         component: AddonModAssignSubmissionListPage, | ||||
|     }, | ||||
|     { | ||||
|         path: ':courseId/:cmId/submission/:submitId', | ||||
|         component: AddonModAssignSubmissionReviewPage, | ||||
|         canDeactivate: [CanLeaveGuard], | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| const tabletRoutes: Routes = [ | ||||
|     ...commonRoutes, | ||||
|     { | ||||
|         path: ':courseId/:cmId/submission', | ||||
|         component: AddonModAssignSubmissionListPage, | ||||
|         children: [ | ||||
|             { | ||||
|                 path: ':submitId', | ||||
|                 component: AddonModAssignSubmissionReviewPage, | ||||
|                 canDeactivate: [CanLeaveGuard], | ||||
|             }, | ||||
|         ], | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     ...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile), | ||||
|     ...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet), | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
|         CoreSharedModule, | ||||
|         AddonModAssignComponentsModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         AddonModAssignIndexPage, | ||||
|         AddonModAssignSubmissionListPage, | ||||
|         AddonModAssignSubmissionReviewPage, | ||||
|         AddonModAssignEditPage, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignLazyModule {} | ||||
							
								
								
									
										70
									
								
								src/addons/mod/assign/assign.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/addons/mod/assign/assign.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { Routes } from '@angular/router'; | ||||
| import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; | ||||
| import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; | ||||
| import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; | ||||
| import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; | ||||
| import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; | ||||
| import { CoreCronDelegate } from '@services/cron'; | ||||
| import { CORE_SITE_SCHEMAS } from '@services/sites'; | ||||
| import { AddonModAssignComponentsModule } from './components/components.module'; | ||||
| import { AddonModAssignFeedbackModule } from './feedback/feedback.module'; | ||||
| import { OFFLINE_SITE_SCHEMA } from './services/database/assign'; | ||||
| import { AddonModAssignIndexLinkHandler } from './services/handlers/index-link'; | ||||
| import { AddonModAssignListLinkHandler } from './services/handlers/list-link'; | ||||
| import { AddonModAssignModuleHandler, AddonModAssignModuleHandlerService } from './services/handlers/module'; | ||||
| import { AddonModAssignPrefetchHandler } from './services/handlers/prefetch'; | ||||
| import { AddonModAssignPushClickHandler } from './services/handlers/push-click'; | ||||
| import { AddonModAssignSyncCronHandler } from './services/handlers/sync-cron'; | ||||
| import { AddonModAssignSubmissionModule } from './submission/submission.module'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: AddonModAssignModuleHandlerService.PAGE_NAME, | ||||
|         loadChildren: () => import('./assign-lazy.module').then(m => m.AddonModAssignLazyModule), | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreMainMenuTabRoutingModule.forChild(routes), | ||||
|         AddonModAssignComponentsModule, | ||||
|         AddonModAssignSubmissionModule, | ||||
|         AddonModAssignFeedbackModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: CORE_SITE_SCHEMAS, | ||||
|             useValue: [OFFLINE_SITE_SCHEMA], | ||||
|             multi: true, | ||||
|         }, | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 CoreCourseModuleDelegate.instance.registerHandler(AddonModAssignModuleHandler.instance); | ||||
|                 CoreContentLinksDelegate.instance.registerHandler(AddonModAssignIndexLinkHandler.instance); | ||||
|                 CoreContentLinksDelegate.instance.registerHandler(AddonModAssignListLinkHandler.instance); | ||||
|                 CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModAssignPrefetchHandler.instance); | ||||
|                 CoreCronDelegate.instance.register(AddonModAssignSyncCronHandler.instance); | ||||
|                 CorePushNotificationsDelegate.instance.registerClickHandler(AddonModAssignPushClickHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignModule {} | ||||
							
								
								
									
										45
									
								
								src/addons/mod/assign/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addons/mod/assign/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreCourseComponentsModule } from '@features/course/components/components.module'; | ||||
| import { AddonModAssignIndexComponent } from './index/index'; | ||||
| import { AddonModAssignSubmissionComponent } from './submission/submission'; | ||||
| import { AddonModAssignSubmissionPluginComponent } from './submission-plugin/submission-plugin'; | ||||
| import { AddonModAssignFeedbackPluginComponent } from './feedback-plugin/feedback-plugin'; | ||||
| import { AddonModAssignEditFeedbackModalComponent } from './edit-feedback-modal/edit-feedback-modal'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModAssignIndexComponent, | ||||
|         AddonModAssignSubmissionComponent, | ||||
|         AddonModAssignSubmissionPluginComponent, | ||||
|         AddonModAssignFeedbackPluginComponent, | ||||
|         AddonModAssignEditFeedbackModalComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|         CoreCourseComponentsModule, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModAssignIndexComponent, | ||||
|         AddonModAssignSubmissionComponent, | ||||
|         AddonModAssignSubmissionPluginComponent, | ||||
|         AddonModAssignFeedbackPluginComponent, | ||||
|         AddonModAssignEditFeedbackModalComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignComponentsModule {} | ||||
| @ -0,0 +1,21 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title>{{ plugin.name }}</ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon slot="icon-only" name="fas-times"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <form name="addon-mod_assign-edit-feedback-form" *ngIf="userId && plugin" #editFeedbackForm> | ||||
|         <addon-mod-assign-feedback-plugin [assign]="assign" [submission]="submission" [userId]="userId" | ||||
|             [plugin]="plugin" [edit]="true"> | ||||
|         </addon-mod-assign-feedback-plugin> | ||||
|         <ion-button expand="block" (click)="done($event)">{{ 'core.done' | translate }}</ion-button> | ||||
|     </form> | ||||
| </ion-content> | ||||
| @ -0,0 +1,100 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| 
 | ||||
| import { Component, Input, ViewChild, ElementRef } from '@angular/core'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { ModalController, Translate } from '@singletons'; | ||||
| import { AddonModAssignAssign, AddonModAssignPlugin, AddonModAssignSubmission } from '../../services/assign'; | ||||
| import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate'; | ||||
| 
 | ||||
| /** | ||||
|  * Modal that allows editing a feedback plugin. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-edit-feedback-modal', | ||||
|     templateUrl: 'edit-feedback-modal.html', | ||||
| }) | ||||
| export class AddonModAssignEditFeedbackModalComponent { | ||||
| 
 | ||||
|     @Input() assign!: AddonModAssignAssign; // The assignment.
 | ||||
|     @Input() submission!: AddonModAssignSubmission; // The submission.
 | ||||
|     @Input() plugin!: AddonModAssignPlugin; // The plugin object.
 | ||||
|     @Input() userId!: number; // The user ID of the submission.
 | ||||
| 
 | ||||
|     @ViewChild('editFeedbackForm') formElement?: ElementRef; | ||||
| 
 | ||||
|     /** | ||||
|      * Close modal checking if there are changes first. | ||||
|      * | ||||
|      * @param data Data to return to the page. | ||||
|      */ | ||||
|     async closeModal(): Promise<void> { | ||||
|         const changed = await this.hasDataChanged(); | ||||
|         if (changed) { | ||||
|             await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit')); | ||||
|         } | ||||
| 
 | ||||
|         CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId()); | ||||
| 
 | ||||
|         ModalController.instance.dismiss(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Done editing. | ||||
|      * | ||||
|      * @param e Click event. | ||||
|      */ | ||||
|     done(e: Event): void { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, false, CoreSites.instance.getCurrentSiteId()); | ||||
| 
 | ||||
|         // Close the modal, sending the input data.
 | ||||
|         ModalController.instance.dismiss(this.getInputData()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the input data. | ||||
|      * | ||||
|      * @return Object with the data. | ||||
|      */ | ||||
|     protected getInputData(): Record<string, unknown> { | ||||
|         return CoreDomUtils.instance.getDataFromForm(document.forms['addon-mod_assign-edit-feedback-form']); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if data has changed. | ||||
|      * | ||||
|      * @return Promise resolved with boolean: whether the data has changed. | ||||
|      */ | ||||
|     protected async hasDataChanged(): Promise<boolean> { | ||||
|         const changed = await CoreUtils.instance.ignoreErrors( | ||||
|             AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged( | ||||
|                 this.assign, | ||||
|                 this.submission, | ||||
|                 this.plugin, | ||||
|                 this.getInputData(), | ||||
|                 this.userId, | ||||
|             ), | ||||
|             true, | ||||
|         ); | ||||
| 
 | ||||
|         return !!changed; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,23 @@ | ||||
| 
 | ||||
| <core-dynamic-component [component]="pluginComponent" [data]="data"> | ||||
|     <!-- This content will be replaced by the component if found. --> | ||||
|     <core-loading [hideUntil]="pluginLoaded"> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="text.length > 0 || files.length > 0"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ plugin.name }}</h2> | ||||
|                 <ion-badge *ngIf="notSupported" color="primary"> | ||||
|                     {{ 'addon.mod_assign.feedbacknotsupported' | translate }} | ||||
|                 </ion-badge> | ||||
|                 <p *ngIf="text"> | ||||
|                     <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" | ||||
|                         [fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid" | ||||
|                         [courseId]="assign.course"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|                 <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" | ||||
|                     [alwaysDownload]="true"> | ||||
|                 </core-file> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|     </core-loading> | ||||
| </core-dynamic-component> | ||||
| @ -0,0 +1,153 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Input, OnInit, ViewChild, Type } from '@angular/core'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { ModalController } from '@singletons'; | ||||
| import { AddonModAssignFeedbackCommentsTextData } from '../../feedback/comments/services/handler'; | ||||
| import { | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssignPlugin, | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssign, | ||||
| } from '../../services/assign'; | ||||
| import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper'; | ||||
| import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate'; | ||||
| import { AddonModAssignEditFeedbackModalComponent } from '../edit-feedback-modal/edit-feedback-modal'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays an assignment feedback plugin. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-feedback-plugin', | ||||
|     templateUrl: 'addon-mod-assign-feedback-plugin.html', | ||||
| }) | ||||
| export class AddonModAssignFeedbackPluginComponent implements OnInit { | ||||
| 
 | ||||
|     @ViewChild(CoreDynamicComponent) dynamicComponent!: CoreDynamicComponent; | ||||
| 
 | ||||
|     @Input() assign!: AddonModAssignAssign; // The assignment.
 | ||||
|     @Input() submission!: AddonModAssignSubmission; // The submission.
 | ||||
|     @Input() plugin!: AddonModAssignPlugin; // The plugin object.
 | ||||
|     @Input() userId!: number; // The user ID of the submission.
 | ||||
|     @Input() canEdit = false; // Whether the user can edit.
 | ||||
|     @Input() edit = false; // Whether the user is editing.
 | ||||
| 
 | ||||
|     pluginComponent?: Type<unknown>; // Component to render the plugin.
 | ||||
|     data?: AddonModAssignFeedbackPluginData; // Data to pass to the component.
 | ||||
| 
 | ||||
|     // Data to render the plugin if it isn't supported.
 | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     text = ''; | ||||
|     files: CoreWSExternalFile[] = []; | ||||
|     notSupported = false; | ||||
|     pluginLoaded = false; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         if (!this.plugin) { | ||||
|             this.pluginLoaded = true; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const name = AddonModAssignFeedbackDelegate.instance.getPluginName(this.plugin); | ||||
| 
 | ||||
|         if (!name) { | ||||
|             this.pluginLoaded = true; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|         this.plugin.name = name; | ||||
| 
 | ||||
|         // Check if the plugin has defined its own component to render itself.
 | ||||
|         this.pluginComponent = await AddonModAssignFeedbackDelegate.instance.getComponentForPlugin(this.plugin); | ||||
| 
 | ||||
|         if (this.pluginComponent) { | ||||
|             // Prepare the data to pass to the component.
 | ||||
|             this.data = { | ||||
|                 assign: this.assign, | ||||
|                 submission: this.submission, | ||||
|                 plugin: this.plugin, | ||||
|                 userId: this.userId, | ||||
|                 configs: AddonModAssignHelper.instance.getPluginConfig(this.assign, 'assignfeedback', this.plugin.type), | ||||
|                 edit: this.edit, | ||||
|                 canEdit: this.canEdit, | ||||
|             }; | ||||
|         } else { | ||||
|             // Data to render the plugin.
 | ||||
|             this.text = AddonModAssign.instance.getSubmissionPluginText(this.plugin); | ||||
|             this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin); | ||||
|             this.notSupported = AddonModAssignFeedbackDelegate.instance.isPluginSupported(this.plugin.type); | ||||
|             this.pluginLoaded = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open a modal to edit the feedback plugin. | ||||
|      * | ||||
|      * @return Promise resolved with the input data, rejected if cancelled. | ||||
|      */ | ||||
|     async editFeedback(): Promise<AddonModAssignFeedbackCommentsTextData> { | ||||
|         if (!this.canEdit) { | ||||
|             throw new CoreError('Cannot edit feedback'); | ||||
|         } | ||||
| 
 | ||||
|         // Create the navigation modal.
 | ||||
|         const modal = await ModalController.instance.create({ | ||||
|             component: AddonModAssignEditFeedbackModalComponent, | ||||
|             componentProps: { | ||||
|                 assign: this.assign, | ||||
|                 submission: this.submission, | ||||
|                 plugin: this.plugin, | ||||
|                 userId: this.userId, | ||||
|             }, | ||||
|         }); | ||||
| 
 | ||||
|         await modal.present(); | ||||
| 
 | ||||
|         const result = await modal.onDidDismiss(); | ||||
| 
 | ||||
|         if (typeof result.data == 'undefined') { | ||||
|             throw null; // User cancelled.
 | ||||
|         } else { | ||||
|             return result.data; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate the plugin data. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async invalidate(): Promise<void> { | ||||
|         await this.dynamicComponent.callComponentFunction('invalidate', []); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export type AddonModAssignFeedbackPluginData = { | ||||
|     assign: AddonModAssignAssign; | ||||
|     submission: AddonModAssignSubmission; | ||||
|     plugin: AddonModAssignPlugin; | ||||
|     configs: AddonModAssignPluginConfig; | ||||
|     edit: boolean; | ||||
|     canEdit: boolean; | ||||
|     userId: number; | ||||
| }; | ||||
| @ -0,0 +1,142 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" | ||||
|             [href]="externalUrl" iconAction="fas-external-link-alt"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="assign && (description || (assign.introattachments && assign.introattachments.length))" | ||||
|             [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" | ||||
|             iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" | ||||
|             iconAction="far-newspaper" (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline"  [priority]="600" | ||||
|             [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" | ||||
|             [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
| <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
| 
 | ||||
|     <!-- Description and intro attachments. --> | ||||
|     <ion-card *ngIf="description" (click)="expandDescription($event)" class="core-clickable"> | ||||
|         <ion-item class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120" | ||||
|                     contextLevel="module" [contextInstanceId]="module!.id" [courseId]="courseId" (click)="expandDescription($event)"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|     </ion-card> | ||||
| 
 | ||||
|     <ion-card *ngIf="assign && assign.introattachments && assign.introattachments.length"> | ||||
|         <core-file *ngFor="let file of assign.introattachments" [file]="file" [component]="component" [componentId]="componentId"> | ||||
|         </core-file> | ||||
|     </ion-card> | ||||
| 
 | ||||
|     <!-- Assign has something offline. --> | ||||
|     <ion-card class="core-warning-card" *ngIf="hasOffline"> | ||||
|         <ion-item> | ||||
|             <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon> | ||||
|             <ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label> | ||||
|         </ion-item> | ||||
|     </ion-card> | ||||
| 
 | ||||
|     <!-- User can view all submissions (teacher). --> | ||||
|     <ng-container *ngIf="assign && canViewAllSubmissions"> | ||||
|         <ion-list class="core-list-align-detail-right with-borders"> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="(groupInfo.separateGroups || groupInfo.visibleGroups)"> | ||||
|                 <ion-label id="addon-assign-groupslabel"> | ||||
|                     <ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container> | ||||
|                     <ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container> | ||||
|                 </ion-label> | ||||
|                 <ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-assign-groupslabel" | ||||
|                     interface="action-sheet"> | ||||
|                     <ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id"> | ||||
|                         {{groupOpt.name}} | ||||
|                     </ion-select-option> | ||||
|                 </ion-select> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap" *ngIf="timeRemaining"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> | ||||
|                     <p>{{ timeRemaining }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="lateSubmissions"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_assign.latesubmissions' | translate }}</h2> | ||||
|                     <p>{{ lateSubmissions }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <!-- Summary of all submissions. --> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="summary && summary.participantcount" (click)="goToSubmissionList()" detail> | ||||
|                 <ion-label> | ||||
|                     <h2 *ngIf="assign.teamsubmission">{{ 'addon.mod_assign.numberofteams' | translate }}</h2> | ||||
|                     <h2 *ngIf="!assign.teamsubmission">{{ 'addon.mod_assign.numberofparticipants' | translate }}</h2> | ||||
|                 </ion-label> | ||||
|                 <ion-badge slot="end" *ngIf="showNumbers" color="primary"> | ||||
|                     {{ summary.participantcount }} | ||||
|                 </ion-badge> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <!-- Summary of submissions with draft status. --> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="assign.submissiondrafts && summary && summary.submissionsenabled" | ||||
|                 [detail]="!showNumbers || summary.submissiondraftscount" | ||||
|                 (click)="goToSubmissionList(submissionStatusDraft, !!summary.submissiondraftscount)"> | ||||
|                 <ion-label><h2>{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</h2></ion-label> | ||||
|                 <ion-badge slot="end" *ngIf="showNumbers" color="primary"> | ||||
|                     {{ summary.submissiondraftscount }} | ||||
|                 </ion-badge> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <!-- Summary of submissions with submitted status. --> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="summary && summary.submissionsenabled" | ||||
|                 [detail]="!showNumbers || summary.submissionssubmittedcount" | ||||
|                 (click)="goToSubmissionList(submissionStatusSubmitted, !!summary.submissionssubmittedcount)"> | ||||
|                 <ion-label><h2>{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</h2></ion-label> | ||||
|                 <ion-badge slot="end" *ngIf="showNumbers" color="primary"> | ||||
|                     {{ summary.submissionssubmittedcount }} | ||||
|                 </ion-badge> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <!-- Summary of submissions that need grading. --> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="summary && summary.submissionsenabled && !assign.teamsubmission && showNumbers" | ||||
|                 [detail]="needsGradingAvalaible" | ||||
|                 (click)="goToSubmissionList(needGrading, needsGradingAvalaible)"> | ||||
|                 <ion-label><h2>{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}</h2></ion-label> | ||||
|                 <ion-badge slot="end" color="primary"> | ||||
|                     {{ summary.submissionsneedgradingcount }} | ||||
|                 </ion-badge> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <!-- Ungrouped users. --> | ||||
|         <ion-card *ngIf="assign.teamsubmission && summary && summary.warnofungroupedusers" class="core-info-card"> | ||||
|             <ion-item> | ||||
|                 <ion-icon name="fas-question-circle" slot="start"></ion-icon> | ||||
|                 <ion-label>{{ 'addon.mod_assign.'+summary.warnofungroupedusers | translate }}</ion-label> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
|     </ng-container> | ||||
| 
 | ||||
|     <!-- If it's a student, display his submission. --> | ||||
|     <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId" | ||||
|         [moduleId]="module!.id"> | ||||
|     </addon-mod-assign-submission> | ||||
| 
 | ||||
| </core-loading> | ||||
							
								
								
									
										424
									
								
								src/addons/mod/assign/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										424
									
								
								src/addons/mod/assign/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,424 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Optional, OnDestroy, OnInit, ViewChild } from '@angular/core'; | ||||
| import { Params } from '@angular/router'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; | ||||
| import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { IonContent } from '@ionic/angular'; | ||||
| import { CoreGroupInfo, CoreGroups } from '@services/groups'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreTimeUtils } from '@services/utils/time'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { | ||||
|     AddonModAssign, | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignGradedEventData, | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssignSubmissionGradingSummary, | ||||
|     AddonModAssignSubmissionSavedEventData, | ||||
|     AddonModAssignSubmittedForGradingEventData, | ||||
| } from '../../services/assign'; | ||||
| import { AddonModAssignOffline } from '../../services/assign-offline'; | ||||
| import { | ||||
|     AddonModAssignAutoSyncData, | ||||
|     AddonModAssignSync, | ||||
|     AddonModAssignSyncProvider, | ||||
|     AddonModAssignSyncResult, | ||||
| } from '../../services/assign-sync'; | ||||
| import { AddonModAssignSubmissionComponent } from '../submission/submission'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays an assignment. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-index', | ||||
|     templateUrl: 'addon-mod-assign-index.html', | ||||
| }) | ||||
| export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|    @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent; | ||||
| 
 | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     moduleName = 'assign'; | ||||
| 
 | ||||
|     assign?: AddonModAssignAssign; // The assign object.
 | ||||
|     canViewAllSubmissions = false; // Whether the user can view all submissions.
 | ||||
|     canViewOwnSubmission = false; // Whether the user can view their own submission.
 | ||||
|     timeRemaining?: string; // Message about time remaining to submit.
 | ||||
|     lateSubmissions?: string; // Message about late submissions.
 | ||||
|     showNumbers = true; // Whether to show number of submissions with each status.
 | ||||
|     summary?: AddonModAssignSubmissionGradingSummary; // The grading summary.
 | ||||
|     needsGradingAvalaible = false; // Whether we can see the submissions that need grading.
 | ||||
| 
 | ||||
|     groupInfo: CoreGroupInfo = { | ||||
|         groups: [], | ||||
|         separateGroups: false, | ||||
|         visibleGroups: false, | ||||
|         defaultGroupId: 0, | ||||
|     }; | ||||
| 
 | ||||
|     // Status.
 | ||||
|     submissionStatusSubmitted = AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED; | ||||
|     submissionStatusDraft = AddonModAssignProvider.SUBMISSION_STATUS_DRAFT; | ||||
|     needGrading = AddonModAssignProvider.NEED_GRADING; | ||||
| 
 | ||||
|     protected currentUserId?: number; // Current user ID.
 | ||||
|     protected currentSite?: CoreSite; // Current user ID.
 | ||||
|     protected syncEventName = AddonModAssignSyncProvider.AUTO_SYNCED; | ||||
| 
 | ||||
|     // Observers.
 | ||||
|     protected savedObserver?: CoreEventObserver; | ||||
|     protected submittedObserver?: CoreEventObserver; | ||||
|     protected gradedObserver?: CoreEventObserver; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected content?: IonContent, | ||||
|         @Optional() courseContentsPage?: CoreCourseContentsPage, | ||||
|     ) { | ||||
|         super('AddonModLessonIndexComponent', content, courseContentsPage); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         super.ngOnInit(); | ||||
| 
 | ||||
|         this.currentUserId = CoreSites.instance.getCurrentSiteUserId(); | ||||
|         this.currentSite = CoreSites.instance.getCurrentSite(); | ||||
| 
 | ||||
|         // Listen to events.
 | ||||
|         this.savedObserver = CoreEvents.on<AddonModAssignSubmissionSavedEventData>( | ||||
|             AddonModAssignProvider.SUBMISSION_SAVED_EVENT, | ||||
|             (data) => { | ||||
|                 if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { | ||||
|                 // Assignment submission saved, refresh data.
 | ||||
|                     this.showLoadingAndRefresh(true, false); | ||||
|                 } | ||||
|             }, | ||||
|             this.siteId, | ||||
|         ); | ||||
| 
 | ||||
|         this.submittedObserver = CoreEvents.on<AddonModAssignSubmittedForGradingEventData>( | ||||
|             AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, | ||||
|             (data) => { | ||||
|                 if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { | ||||
|                 // Assignment submitted, check completion.
 | ||||
|                     CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata); | ||||
| 
 | ||||
|                     // Reload data since it can have offline data now.
 | ||||
|                     this.showLoadingAndRefresh(true, false); | ||||
|                 } | ||||
|             }, | ||||
|             this.siteId, | ||||
|         ); | ||||
| 
 | ||||
|         this.gradedObserver = CoreEvents.on<AddonModAssignGradedEventData>(AddonModAssignProvider.GRADED_EVENT, (data) => { | ||||
|             if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { | ||||
|                 // Assignment graded, refresh data.
 | ||||
|                 this.showLoadingAndRefresh(true, false); | ||||
|             } | ||||
|         }, this.siteId); | ||||
| 
 | ||||
|         await this.loadContent(false, true); | ||||
| 
 | ||||
|         try { | ||||
|             await AddonModAssign.instance.logView(this.assign!.id, this.assign!.name); | ||||
|             CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata); | ||||
|         } catch { | ||||
|             // Ignore errors. Just don't check Module completion.
 | ||||
|         } | ||||
| 
 | ||||
|         if (this.canViewAllSubmissions) { | ||||
|             // User can see all submissions, log grading view.
 | ||||
|             CoreUtils.instance.ignoreErrors(AddonModAssign.instance.logGradingView(this.assign!.id, this.assign!.name)); | ||||
|         } else if (this.canViewOwnSubmission) { | ||||
|             // User can only see their own submission, log view the user submission.
 | ||||
|             CoreUtils.instance.ignoreErrors(AddonModAssign.instance.logSubmissionView(this.assign!.id, this.assign!.name)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Expand the description. | ||||
|      */ | ||||
|     expandDescription(ev?: Event): void { | ||||
|         ev?.preventDefault(); | ||||
|         ev?.stopPropagation(); | ||||
| 
 | ||||
|         if (this.assign && (this.description || this.assign.introattachments)) { | ||||
|             CoreTextUtils.instance.viewText(Translate.instance.instant('core.description'), this.description || '', { | ||||
|                 component: this.component, | ||||
|                 componentId: this.module!.id, | ||||
|                 files: this.assign.introattachments, | ||||
|                 filter: true, | ||||
|                 contextLevel: 'module', | ||||
|                 instanceId: this.module!.id, | ||||
|                 courseId: this.courseId, | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get assignment data. | ||||
|      * | ||||
|      * @param refresh If it's refreshing content. | ||||
|      * @param sync If it should try to sync. | ||||
|      * @param showErrors If show errors to the user of hide them. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> { | ||||
| 
 | ||||
|         // Get assignment data.
 | ||||
|         try { | ||||
|             this.assign = await AddonModAssign.instance.getAssignment(this.courseId!, this.module!.id); | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.assign); | ||||
|             this.description = this.assign.intro; | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 // Try to synchronize the assign.
 | ||||
|                 await CoreUtils.instance.ignoreErrors(this.syncActivity(showErrors)); | ||||
|             } | ||||
| 
 | ||||
|             // Check if there's any offline data for this assign.
 | ||||
|             this.hasOffline = await AddonModAssignOffline.instance.hasAssignOfflineData(this.assign.id); | ||||
| 
 | ||||
|             // Get assignment submissions.
 | ||||
|             const submissions = await AddonModAssign.instance.getSubmissions(this.assign.id, { cmId: this.module!.id }); | ||||
|             const time = CoreTimeUtils.instance.timestamp(); | ||||
| 
 | ||||
|             this.canViewAllSubmissions = submissions.canviewsubmissions; | ||||
| 
 | ||||
|             if (submissions.canviewsubmissions) { | ||||
| 
 | ||||
|                 // Calculate the messages to display about time remaining and late submissions.
 | ||||
|                 if (this.assign.duedate > 0) { | ||||
|                     if (this.assign.duedate - time <= 0) { | ||||
|                         this.timeRemaining = Translate.instance.instant('addon.mod_assign.assignmentisdue'); | ||||
|                     } else { | ||||
|                         this.timeRemaining = CoreTimeUtils.instance.formatDuration(this.assign.duedate - time, 3); | ||||
| 
 | ||||
|                         if (this.assign.cutoffdate) { | ||||
|                             if (this.assign.cutoffdate > time) { | ||||
|                                 this.lateSubmissions = Translate.instance.instant( | ||||
|                                     'addon.mod_assign.latesubmissionsaccepted', | ||||
|                                     { $a: CoreTimeUtils.instance.userDate(this.assign.cutoffdate * 1000) }, | ||||
|                                 ); | ||||
|                             } else { | ||||
|                                 this.lateSubmissions = Translate.instance.instant('addon.mod_assign.nomoresubmissionsaccepted'); | ||||
|                             } | ||||
|                         } else { | ||||
|                             this.lateSubmissions = ''; | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     this.timeRemaining = ''; | ||||
|                     this.lateSubmissions = ''; | ||||
|                 } | ||||
| 
 | ||||
|                 // Check if groupmode is enabled to avoid showing wrong numbers.
 | ||||
|                 this.groupInfo = await CoreGroups.instance.getActivityGroupInfo(this.assign.cmid, false); | ||||
|                 this.showNumbers = (this.groupInfo.groups && this.groupInfo.groups.length == 0) || | ||||
|                     this.currentSite!.isVersionGreaterEqualThan('3.5'); | ||||
| 
 | ||||
|                 await this.setGroup(CoreGroups.instance.validateGroupId(this.group, this.groupInfo)); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 // Check if the user can view their own submission.
 | ||||
|                 await AddonModAssign.instance.getSubmissionStatus(this.assign.id, { cmId: this.module!.id }); | ||||
|                 this.canViewOwnSubmission = true; | ||||
|             } catch (error) { | ||||
|                 this.canViewOwnSubmission = false; | ||||
| 
 | ||||
|                 if (error.errorcode !== 'nopermission') { | ||||
|                     throw error; | ||||
|                 } | ||||
|             } | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set group to see the summary. | ||||
|      * | ||||
|      * @param groupId Group ID. | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     async setGroup(groupId = 0): Promise<void> { | ||||
|         this.group = groupId; | ||||
| 
 | ||||
|         const submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign!.id, { | ||||
|             groupId: this.group, | ||||
|             cmId: this.module!.id, | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         this.summary = submissionStatus.gradingsummary; | ||||
|         if (!this.summary) { | ||||
|             this.needsGradingAvalaible = false; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (this.summary?.warnofungroupedusers === true) { | ||||
|             this.summary.warnofungroupedusers = 'ungroupedusers'; | ||||
|         } else { | ||||
|             switch (this.summary?.warnofungroupedusers) { | ||||
|                 case AddonModAssignProvider.WARN_GROUPS_REQUIRED: | ||||
|                     this.summary.warnofungroupedusers = 'ungroupedusers'; | ||||
|                     break; | ||||
|                 case AddonModAssignProvider.WARN_GROUPS_OPTIONAL: | ||||
|                     this.summary.warnofungroupedusers = 'ungroupedusersoptional'; | ||||
|                     break; | ||||
|                 default: | ||||
|                     this.summary.warnofungroupedusers = ''; | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.needsGradingAvalaible = | ||||
|             (submissionStatus.gradingsummary?.submissionsneedgradingcount || 0) > 0 && | ||||
|             this.currentSite!.isVersionGreaterEqualThan('3.2'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Go to view a list of submissions. | ||||
|      * | ||||
|      * @param status Status to see. | ||||
|      * @param hasSubmissions If the status has any submission. | ||||
|      */ | ||||
|     goToSubmissionList(status?: string, hasSubmissions = false): void { | ||||
|         if (typeof status != 'undefined' && !hasSubmissions && this.showNumbers) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const params: Params = { | ||||
|             groupId: this.group || 0, | ||||
|             moduleName: this.moduleName, | ||||
|         }; | ||||
|         if (typeof status != 'undefined') { | ||||
|             params.status = status; | ||||
|         } | ||||
|         CoreNavigator.instance.navigate('submission', { | ||||
|             params, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if sync has succeed from result sync data. | ||||
|      * | ||||
|      * @param result Data returned by the sync function. | ||||
|      * @return If succeed or not. | ||||
|      */ | ||||
|     protected hasSyncSucceed(result: AddonModAssignSyncResult): boolean { | ||||
|         if (result.updated) { | ||||
|             this.submissionComponent?.invalidateAndRefresh(false); | ||||
|         } | ||||
| 
 | ||||
|         return result.updated; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the invalidate content function. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async invalidateContent(): Promise<void> { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId!)); | ||||
| 
 | ||||
|         if (this.assign) { | ||||
|             promises.push(AddonModAssign.instance.invalidateAllSubmissionData(this.assign.id)); | ||||
| 
 | ||||
|             if (this.canViewAllSubmissions) { | ||||
|                 promises.push(AddonModAssign.instance.invalidateSubmissionStatusData(this.assign.id, undefined, this.group)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         await Promise.all(promises).finally(() => { | ||||
|             this.submissionComponent?.invalidateAndRefresh(true); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User entered the page that contains the component. | ||||
|      */ | ||||
|     ionViewDidEnter(): void { | ||||
|         super.ionViewDidEnter(); | ||||
| 
 | ||||
|         this.submissionComponent?.ionViewDidEnter(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User left the page that contains the component. | ||||
|      */ | ||||
|     ionViewDidLeave(): void { | ||||
|         super.ionViewDidLeave(); | ||||
| 
 | ||||
|         this.submissionComponent?.ionViewDidLeave(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Compares sync event data with current data to check if refresh content is needed. | ||||
|      * | ||||
|      * @param syncEventData Data receiven on sync observer. | ||||
|      * @return True if refresh is needed, false otherwise. | ||||
|      */ | ||||
|     protected isRefreshSyncNeeded(syncEventData: AddonModAssignAutoSyncData): boolean { | ||||
|         if (this.assign && syncEventData.assignId == this.assign.id) { | ||||
|             if (syncEventData.warnings && syncEventData.warnings.length) { | ||||
|                 // Show warnings.
 | ||||
|                 CoreDomUtils.instance.showErrorModal(syncEventData.warnings[0]); | ||||
|             } | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Performs the sync of the activity. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async sync(): Promise<void> { | ||||
|         await AddonModAssignSync.instance.syncAssign(this.assign!.id); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         super.ngOnDestroy(); | ||||
| 
 | ||||
|         this.savedObserver?.off(); | ||||
|         this.submittedObserver?.off(); | ||||
|         this.gradedObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,23 @@ | ||||
| 
 | ||||
| <core-dynamic-component [component]="pluginComponent" [data]="data"> | ||||
|     <!-- This content will be replaced by the component if found. --> | ||||
|     <core-loading [hideUntil]="pluginLoaded"> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="text.length > 0 || files.length > 0"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ plugin.name }}</h2> | ||||
|                 <ion-badge *ngIf="notSupported" color="primary"> | ||||
|                     {{ 'addon.mod_assign.submissionnotsupported' | translate }} | ||||
|                 </ion-badge> | ||||
|                 <p *ngIf="text"> | ||||
|                     <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" | ||||
|                         [fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid" | ||||
|                         [courseId]="assign.course"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|                 <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" | ||||
|                     [alwaysDownload]="true"> | ||||
|                 </core-file> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|     </core-loading> | ||||
| </core-dynamic-component> | ||||
| @ -0,0 +1,115 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Input, OnInit, Type, ViewChild } from '@angular/core'; | ||||
| import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssignPlugin, | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssign, | ||||
| } from '../../services/assign'; | ||||
| import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper'; | ||||
| import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate'; | ||||
| import { FileEntry } from '@ionic-native/file/ngx'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays an assignment submission plugin. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-submission-plugin', | ||||
|     templateUrl: 'addon-mod-assign-submission-plugin.html', | ||||
| }) | ||||
| export class AddonModAssignSubmissionPluginComponent implements OnInit { | ||||
| 
 | ||||
|     @ViewChild(CoreDynamicComponent) dynamicComponent!: CoreDynamicComponent; | ||||
| 
 | ||||
|     @Input() assign!: AddonModAssignAssign; // The assignment.
 | ||||
|     @Input() submission!: AddonModAssignSubmission; // The submission.
 | ||||
|     @Input() plugin!: AddonModAssignPlugin; // The plugin object.
 | ||||
|     @Input() edit = false; // Whether the user is editing.
 | ||||
|     @Input() allowOffline = false; // Whether to allow offline.
 | ||||
| 
 | ||||
|     pluginComponent?: Type<unknown>; // Component to render the plugin.
 | ||||
|     data?: AddonModAssignSubmissionPluginData; // Data to pass to the component.
 | ||||
| 
 | ||||
|     // Data to render the plugin if it isn't supported.
 | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     text = ''; | ||||
|     files: (FileEntry | CoreWSExternalFile)[] = []; | ||||
|     notSupported = false; | ||||
|     pluginLoaded = false; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         if (!this.plugin) { | ||||
|             this.pluginLoaded = true; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const name = AddonModAssignSubmissionDelegate.instance.getPluginName(this.plugin); | ||||
| 
 | ||||
|         if (!name) { | ||||
|             this.pluginLoaded = true; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|         this.plugin.name = name; | ||||
| 
 | ||||
|         // Check if the plugin has defined its own component to render itself.
 | ||||
|         this.pluginComponent = await AddonModAssignSubmissionDelegate.instance.getComponentForPlugin(this.plugin, this.edit); | ||||
| 
 | ||||
|         if (this.pluginComponent) { | ||||
|             // Prepare the data to pass to the component.
 | ||||
|             this.data = { | ||||
|                 assign: this.assign, | ||||
|                 submission: this.submission, | ||||
|                 plugin: this.plugin, | ||||
|                 configs: AddonModAssignHelper.instance.getPluginConfig(this.assign, 'assignsubmission', this.plugin.type), | ||||
|                 edit: this.edit, | ||||
|                 allowOffline: this.allowOffline, | ||||
|             }; | ||||
|         } else { | ||||
|             // Data to render the plugin.
 | ||||
|             this.text = AddonModAssign.instance.getSubmissionPluginText(this.plugin); | ||||
|             this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin); | ||||
|             this.notSupported = AddonModAssignSubmissionDelegate.instance.isPluginSupported(this.plugin.type); | ||||
|             this.pluginLoaded = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate the plugin data. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async invalidate(): Promise<void> { | ||||
|         await this.dynamicComponent.callComponentFunction('invalidate', []); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export type AddonModAssignSubmissionPluginData = { | ||||
|     assign: AddonModAssignAssign; | ||||
|     submission: AddonModAssignSubmission; | ||||
|     plugin: AddonModAssignPlugin; | ||||
|     configs: AddonModAssignPluginConfig; | ||||
|     edit: boolean; | ||||
|     allowOffline: boolean; | ||||
| }; | ||||
| @ -0,0 +1,395 @@ | ||||
| <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
| 
 | ||||
|     <!-- User and status of the submission. --> | ||||
|     <ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId" | ||||
|         [title]="user!.fullname"> | ||||
|         <core-user-avatar [user]="user" slot="start"></core-user-avatar> | ||||
|         <ion-label> | ||||
|             <h2>{{ user!.fullname }}</h2> | ||||
|             <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> | ||||
|         </ion-label> | ||||
|         <ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container> | ||||
|     </ion-item> | ||||
| 
 | ||||
|     <!-- Status of the submission if user is blinded. --> | ||||
|     <ion-item class="ion-text-wrap" *ngIf="blindMarking && !user"> | ||||
|         <ion-label> | ||||
|             <h2>{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</h2> | ||||
|             <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> | ||||
|         </ion-label> | ||||
|         <ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container> | ||||
|     </ion-item> | ||||
| 
 | ||||
|     <!-- Status of the submission in the rest of cases. --> | ||||
|     <ion-item class="ion-text-wrap" *ngIf="(blindMarking && user) || (!blindMarking && !user)"> | ||||
|         <ion-label> | ||||
|             <h2>{{ 'addon.mod_assign.submissionstatus' | translate }}</h2> | ||||
|             <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> | ||||
|         </ion-label> | ||||
|         <ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container> | ||||
|     </ion-item> | ||||
| 
 | ||||
|     <!-- Tabs: see the submission or grade it. --> | ||||
|     <core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true" (ionChange)="tabSelected($event)"> | ||||
|         <!-- View the submission tab. --> | ||||
|         <core-tab [title]="'addon.mod_assign.submission' | translate" id="submission"> | ||||
|             <ng-template> | ||||
|                 <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" | ||||
|                     [assign]="assign" [submission]="userSubmission" [plugin]="plugin"> | ||||
|                 </addon-mod-assign-submission-plugin> | ||||
| 
 | ||||
|                 <!-- Render some data about the submission. --> | ||||
|                 <ion-item class="ion-text-wrap" | ||||
|                     *ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2> | ||||
|                         <p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="timeRemaining" [ngClass]="[timeRemainingClass]"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> | ||||
|                         <p [innerHTML]="timeRemaining"></p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="fromDate && !isSubmittedForGrading"> | ||||
|                     <ion-label> | ||||
|                         <p *ngIf="assign!.intro" | ||||
|                             [innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}"> | ||||
|                         </p> | ||||
|                         <p *ngIf="!assign!.intro" | ||||
|                             [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate: | ||||
|                                 {'$a': fromDate}"> | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && !isSubmittedForGrading"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.duedate' | translate }}</h2> | ||||
|                         <p *ngIf="assign!.duedate" >{{ assign!.duedate * 1000 | coreFormatDate }}</p> | ||||
|                         <p *ngIf="!assign!.duedate" >{{ 'addon.mod_assign.duedateno' | translate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && assign!.cutoffdate && isSubmittedForGrading"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2> | ||||
|                         <p>{{ assign!.cutoffdate * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" | ||||
|                     *ngIf="assign!.duedate && lastAttempt?.extensionduedate && !isSubmittedForGrading"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2> | ||||
|                         <p>{{ lastAttempt!.extensionduedate * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2> | ||||
|                         <p *ngIf="assign!.maxattempts == unlimitedAttempts"> | ||||
|                             {{ 'addon.mod_assign.outof' | translate : | ||||
|                                 {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} | ||||
|                         </p> | ||||
|                         <p *ngIf="assign!.maxattempts != unlimitedAttempts"> | ||||
|                             {{ 'addon.mod_assign.outof' | translate : | ||||
|                                 {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Add or edit submission. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="canEdit"> | ||||
|                     <ion-label> | ||||
|                         <div *ngIf="!unsupportedEditPlugins.length && !showErrorStatementEdit"> | ||||
|                             <!-- If has offline data, show edit. --> | ||||
|                             <ion-button expand="block" class="ion-text-wrap" color="primary" *ngIf="hasOffline" | ||||
|                                 (click)="goToEdit()"> | ||||
|                                 {{ 'addon.mod_assign.editsubmission' | translate }} | ||||
|                             </ion-button> | ||||
|                             <!-- If no submission or is new, show add submission. --> | ||||
|                             <ion-button expand="block" class="ion-text-wrap" color="primary" | ||||
|                                 *ngIf="!hasOffline && | ||||
|                                     (!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)" | ||||
|                                 (click)="goToEdit()"> | ||||
|                                 {{ 'addon.mod_assign.addsubmission' | translate }} | ||||
|                             </ion-button> | ||||
|                             <!-- If reopened, show addfromprevious and addnewattempt. --> | ||||
|                             <ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened"> | ||||
|                                 <ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap" color="primary" | ||||
|                                     (click)="copyPrevious()"> | ||||
|                                     {{ 'addon.mod_assign.addnewattemptfromprevious' | translate }} | ||||
|                                 </ion-button> | ||||
|                                 <ion-button expand="block" class="ion-text-wrap" color="primary" (click)="goToEdit()"> | ||||
|                                     {{ 'addon.mod_assign.addnewattempt' | translate }} | ||||
|                                 </ion-button> | ||||
|                             </ng-container> | ||||
|                             <!-- Else show editsubmission. --> | ||||
|                             <ion-button expand="block" class="ion-text-wrap" color="primary" | ||||
|                                 *ngIf="!hasOffline && userSubmission && userSubmission!.status && | ||||
|                                     userSubmission!.status != statusNew && | ||||
|                                     userSubmission!.status != statusReopened" (click)="goToEdit()"> | ||||
|                                 {{ 'addon.mod_assign.editsubmission' | translate }} | ||||
|                             </ion-button> | ||||
|                         </div> | ||||
|                         <div *ngIf="unsupportedEditPlugins && unsupportedEditPlugins.length && !showErrorStatementEdit"> | ||||
|                             <p class="core-danger-item">{{ 'addon.mod_assign.erroreditpluginsnotsupported' | translate }}</p> | ||||
|                             <p class="core-danger-item" *ngFor="let name of unsupportedEditPlugins">{{ name }}</p> | ||||
|                         </div> | ||||
|                         <div *ngIf="showErrorStatementEdit"> | ||||
|                             <p class="core-danger-item">{{ 'addon.mod_assign.cannoteditduetostatementsubmission' | translate }}</p> | ||||
|                         </div> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Submit for grading form. --> | ||||
|                 <ng-container *ngIf="canSubmit"> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="submissionStatement"> | ||||
|                         <ion-label> | ||||
|                             <core-format-text [text]="submissionStatement" [filter]="false"></core-format-text> | ||||
|                         </ion-label> | ||||
|                         <ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="acceptStatement"> | ||||
|                         </ion-checkbox> | ||||
|                     </ion-item> | ||||
|                     <!-- Submit button. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="!showErrorStatementSubmit"> | ||||
|                         <ion-label> | ||||
|                             <ion-button expand="block" class="ion-text-wrap" | ||||
|                                 (click)="submitForGrading(acceptStatement)"> | ||||
|                                 {{ 'addon.mod_assign.submitassignment' | translate }} | ||||
|                             </ion-button> | ||||
|                             <p>{{ 'addon.mod_assign.submitassignment_help' | translate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                     <!-- Error because we lack submissions statement. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="showErrorStatementSubmit"> | ||||
|                         <ion-label> | ||||
|                             <p class="core-danger-item"> | ||||
|                                 {{ 'addon.mod_assign.cannotsubmitduetostatementsubmission' | translate }} | ||||
|                             </p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <!-- Team members that need to submit it too. --> | ||||
|                 <ion-item-divider class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0"> | ||||
|                     <ion-label><h2>{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}</h2></ion-label> | ||||
|                 </ion-item-divider> | ||||
|                 <ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && !blindMarking"> | ||||
|                     <ng-container *ngFor="let user of membersToSubmit"> | ||||
|                         <ion-item class="ion-text-wrap" core-user-link [userId]="user.id" | ||||
|                             [courseId]="courseId" [title]="user.fullname"> | ||||
|                             <core-user-avatar [user]="user" slot="start"></core-user-avatar> | ||||
|                             <ion-label><h2>{{ user.fullname }}</h2></ion-label> | ||||
|                         </ion-item> | ||||
|                     </ng-container> | ||||
|                 </ng-container> | ||||
|                 <ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && blindMarking"> | ||||
|                     <ng-container *ngFor="let blindId of membersToSubmitBlind"> | ||||
|                         <ion-item class="ion-text-wrap"> | ||||
|                             <ion-label>{{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }}</ion-label> | ||||
|                         </ion-item> | ||||
|                     </ng-container> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <!-- Submission is locked. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked"> | ||||
|                     <ion-label><h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2></ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Editing status. --> | ||||
|                 <ion-item class="ion-text-wrap" | ||||
|                     *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined" | ||||
|                     [ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2> | ||||
|                         <p *ngIf="lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p> | ||||
|                         <p *ngIf="!lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
| 
 | ||||
|         <!-- Grade the submission tab. --> | ||||
|         <core-tab [title]="'addon.mod_assign.grade' | translate" *ngIf="feedback || isGrading" id="grade"> | ||||
|             <ng-template> | ||||
|                 <!-- Current grade if method is advanced. --> | ||||
|                 <ion-item class="ion-text-wrap core-grading-summary" | ||||
|                     *ngIf="feedback?.gradefordisplay && (!isGrading || grade.method != 'simple')"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2> | ||||
|                         <p><core-format-text [text]="feedback!.gradefordisplay" [filter]="false"></core-format-text></p> | ||||
|                     </ion-label> | ||||
|                     <ion-button slot="end" *ngIf="feedback!.advancedgrade" (click)="showAdvancedGrade()"> | ||||
|                         <ion-icon name="fas-search" slot="icon-only"></ion-icon> | ||||
|                     </ion-button> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ng-container *ngIf="isGrading"> | ||||
|                     <!-- Numeric grade. | ||||
|                         Use a text input because otherwise we cannot readthe value if it has an invalid character. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && !grade.scale"> | ||||
|                         <ion-label position="stacked"> | ||||
|                             <h2>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</h2> | ||||
|                         </ion-label> | ||||
|                         <ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade" | ||||
|                             [lang]="grade.lang"> | ||||
|                         </ion-input> | ||||
|                         <p item-content *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p> | ||||
|                     </ion-item> | ||||
| 
 | ||||
|                     <!-- Grade using a scale. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && grade.scale"> | ||||
|                         <ion-label><h2>{{ 'addon.mod_assign.grade' | translate }}</h2></ion-label> | ||||
|                         <ion-select [(ngModel)]="grade.grade" interface="action-sheet" [disabled]="grade.disabled"> | ||||
|                             <ion-select-option *ngFor="let grade of grade.scale" [value]="grade.value"> | ||||
|                                 {{grade.label}} | ||||
|                             </ion-select-option> | ||||
|                         </ion-select> | ||||
|                     </ion-item> | ||||
| 
 | ||||
|                     <!-- Outcomes. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes"> | ||||
|                         <ion-label><h2>{{ outcome.name }}</h2></ion-label> | ||||
|                         <ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId" | ||||
|                             interface="action-sheet" [disabled]="gradeInfo!.disabled"> | ||||
|                             <ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value"> | ||||
|                                 {{grade.label}} | ||||
|                             </ion-select-option> | ||||
|                         </ion-select> | ||||
|                         <p item-content *ngIf="!canSaveGrades || !outcome.itemNumber">{{ outcome.selected }}</p> | ||||
|                     </ion-item> | ||||
| 
 | ||||
|                     <!-- Gradebook grade for simple grading. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple'"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2> | ||||
|                             <p *ngIf="grade.gradebookGrade && !grade.scale"> | ||||
|                                 {{ grade.gradebookGrade }} | ||||
|                             </p> | ||||
|                             <p *ngIf="grade.gradebookGrade && grade.scale"> | ||||
|                                 {{ grade.scale[grade.gradebookGrade].label }} | ||||
|                             </p> | ||||
|                             <p *ngIf="!grade.gradebookGrade">-</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback!.plugins" [assign]="assign" | ||||
|                     [submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades"> | ||||
|                 </addon-mod-assign-feedback-plugin> | ||||
| 
 | ||||
|                 <!-- Workflow status. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="workflowStatusTranslationId"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.markingworkflowstate' | translate }}</h2> | ||||
|                         <p>{{ workflowStatusTranslationId | translate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!--- Apply grade to all team members. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2> | ||||
|                         <p>{{ 'addon.mod_assign.applytoteam' | translate }}</p> | ||||
|                     </ion-label> | ||||
|                     <ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Attempt status. --> | ||||
|                 <ng-container *ngIf="isGrading && assign!.attemptreopenmethod != attemptReopenMethodNone"> | ||||
|                     <ion-item class="ion-text-wrap"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2> | ||||
|                             <p *ngIf="assign!.maxattempts == unlimitedAttempts"> | ||||
|                                 {{ 'addon.mod_assign.outof' | translate : | ||||
|                                     {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} | ||||
|                             </p> | ||||
|                             <p *ngIf="assign!.maxattempts != unlimitedAttempts"> | ||||
|                                 {{ 'addon.mod_assign.outof' | translate : | ||||
|                                     {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} | ||||
|                             </p> | ||||
|                             <p> | ||||
|                                 {{ 'addon.mod_assign.attemptreopenmethod' | translate }}: | ||||
|                                 {{ 'addon.mod_assign.attemptreopenmethod_' + assign!.attemptreopenmethod | translate }} | ||||
|                             </p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                     <ion-item *ngIf="canSaveGrades && allowAddAttempt" > | ||||
|                         <ion-label>{{ 'addon.mod_assign.addattempt' | translate }}</ion-label> | ||||
|                         <ion-toggle [(ngModel)]="grade.addAttempt"></ion-toggle> | ||||
|                     </ion-item> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <!-- Data about the grader (teacher who graded). --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader!.id" [courseId]="courseId" | ||||
|                     [title]="grader!.fullname" detail="true"> | ||||
|                     <core-user-avatar [user]="grader" slot="start"></core-user-avatar> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2> | ||||
|                         <h2>{{ grader!.fullname }}</h2> | ||||
|                         <p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Grader is hidden, display only the grade date. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="!grader && feedback!.gradeddate"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.gradedon' | translate }}</h2> | ||||
|                         <p>{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Warning message if cannot save grades. --> | ||||
|                 <ion-card *ngIf="isGrading && !canSaveGrades" class="core-warning-card"> | ||||
|                     <ion-item> | ||||
|                         <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon> | ||||
|                         <ion-label> | ||||
|                             <p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p> | ||||
|                             <ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link > | ||||
|                                 {{ 'core.openinbrowser' | translate }} | ||||
|                                 <ion-icon name="fas-external-link-alt" slot="end"></ion-icon> | ||||
|                             </ion-button> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 </ion-card> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
|     </core-tabs> | ||||
| </core-loading> | ||||
| 
 | ||||
| <!-- Template to render some data regarding the submission status. --> | ||||
| <ng-template #submissionStatus> | ||||
|     <ng-container *ngIf="assign && assign!.teamsubmission && lastAttempt"> | ||||
|         <p *ngIf="lastAttempt!.submissiongroup && lastAttempt!.submissiongroupname">{{lastAttempt!.submissiongroupname}}</p> | ||||
|         <ng-container *ngIf="assign!.preventsubmissionnotingroup && | ||||
|             !lastAttempt!.submissiongroup && | ||||
|             (!lastAttempt!.usergroups || lastAttempt!.usergroups.length <= 0)"> | ||||
|             <p class="text-danger"><strong>{{ 'addon.mod_assign.noteam' | translate }}</strong></p> | ||||
|             <p class="text-danger">{{ 'addon.mod_assign.noteam_desc' | translate }}</p> | ||||
|         </ng-container> | ||||
|         <ng-container *ngIf="assign!.preventsubmissionnotingroup && | ||||
|             !lastAttempt!.submissiongroup && | ||||
|             lastAttempt!.usergroups && | ||||
|             lastAttempt!.usergroups.length > 1"> | ||||
|             <p class="text-danger"><strong>{{ 'addon.mod_assign.multipleteams' | translate }}</strong></p> | ||||
|             <p class="text-danger">{{ 'addon.mod_assign.multipleteams_desc' | translate }}</p> | ||||
|         </ng-container> | ||||
|         <p *ngIf="!assign!.preventsubmissionnotingroup && !lastAttempt!.submissiongroup"> | ||||
|             {{ 'addon.mod_assign.defaultteam' | translate }} | ||||
|         </p> | ||||
|     </ng-container> | ||||
| </ng-template> | ||||
| <ng-template #submissionStatusBadges> | ||||
|     <ion-badge slot="end" *ngIf="statusTranslated" [color]="statusColor"> | ||||
|         {{ statusTranslated }} | ||||
|     </ion-badge> | ||||
|     <ion-badge slot="end" *ngIf="gradingStatusTranslationId" [color]="gradingColor"> | ||||
|         {{ gradingStatusTranslationId | translate }} | ||||
|     </ion-badge> | ||||
| </ng-template> | ||||
							
								
								
									
										30
									
								
								src/addons/mod/assign/components/submission/submission.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/addons/mod/assign/components/submission/submission.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| :host ::ng-deep { | ||||
|     div.latesubmission, | ||||
|     div.overdue { | ||||
|         border-bottom: 3px solid var(--danger) !important; | ||||
|         ion-icon { | ||||
|             color: var(--danger); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     div.earlysubmission { | ||||
|         border-bottom: 3px solid var(--success) !important; | ||||
|         ion-icon { | ||||
|             color: var(--success); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     div.submissioneditable p { | ||||
|         color: var(--red); | ||||
|     } | ||||
| 
 | ||||
|     .core-grading-summary .advancedgrade { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| :host-context(body.dark) ::ng-deep { | ||||
|     div.submissioneditable p { | ||||
|         color: var(--red-light); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										1225
									
								
								src/addons/mod/assign/components/submission/submission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1225
									
								
								src/addons/mod/assign/components/submission/submission.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										47
									
								
								src/addons/mod/assign/feedback/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/addons/mod/assign/feedback/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { AddonModAssignFeedbackCommentsHandler } from './services/handler'; | ||||
| import { AddonModAssignFeedbackCommentsComponent } from './component/comments'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreEditorComponentsModule } from '@features/editor/components/components.module'; | ||||
| import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModAssignFeedbackCommentsComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|         CoreEditorComponentsModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonModAssignFeedbackDelegate.instance.registerHandler(AddonModAssignFeedbackCommentsHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModAssignFeedbackCommentsComponent, | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonModAssignFeedbackCommentsComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignFeedbackCommentsModule {} | ||||
| @ -0,0 +1,33 @@ | ||||
| <!-- Read only. --> | ||||
| <ion-item class="ion-text-wrap" *ngIf="(text || canEdit) && !edit"> | ||||
|     <ion-label> | ||||
|         <h2>{{ plugin.name }}</h2> | ||||
|         <p> | ||||
|             <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" | ||||
|                 [fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid" | ||||
|                 [courseId]="assign.course"> | ||||
|             </core-format-text> | ||||
|         </p> | ||||
|     </ion-label> | ||||
|     <div slot="end"> | ||||
|         <div class="ion-text-end"> | ||||
|             <ion-button fill="clear" *ngIf="canEdit" (click)="editComment()" color="dark"> | ||||
|                 <ion-icon name="fas-pen" slot="icon-only"></ion-icon> | ||||
|             </ion-button> | ||||
|         </div> | ||||
|         <ion-note *ngIf="!isSent" color="dark"> | ||||
|             <ion-icon name="far-clock"></ion-icon> | ||||
|             {{ 'core.notsent' | translate }} | ||||
|         </ion-note> | ||||
|     </div> | ||||
| </ion-item> | ||||
| 
 | ||||
| <!-- Edit --> | ||||
| <ion-item class="ion-text-wrap" *ngIf="edit && loaded"> | ||||
|     <ion-label></ion-label> | ||||
|     <core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" | ||||
|         name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true" | ||||
|         contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor" | ||||
|         [draftExtraParams]="{userid: userId, action: 'grade'}"> | ||||
|     </core-rich-text-editor> | ||||
| </ion-item> | ||||
							
								
								
									
										161
									
								
								src/addons/mod/assign/feedback/comments/component/comments.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/addons/mod/assign/feedback/comments/component/comments.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,161 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, ElementRef } from '@angular/core'; | ||||
| import { FormBuilder, FormControl } from '@angular/forms'; | ||||
| import { AddonModAssignFeedbackPluginComponent } from '@addons/mod/assign/components/feedback-plugin/feedback-plugin'; | ||||
| import { AddonModAssign, AddonModAssignProvider } from '@addons/mod/assign/services/assign'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { | ||||
|     AddonModAssignFeedbackCommentsDraftData, | ||||
|     AddonModAssignFeedbackCommentsHandler, | ||||
|     AddonModAssignFeedbackCommentsPluginData, | ||||
| } from '../services/handler'; | ||||
| import { AddonModAssignFeedbackDelegate } from '@addons/mod/assign/services/feedback-delegate'; | ||||
| import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| /** | ||||
|  * Component to render a comments feedback plugin. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-feedback-comments', | ||||
|     templateUrl: 'addon-mod-assign-feedback-comments.html', | ||||
| }) | ||||
| export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedbackPluginComponent implements OnInit { | ||||
| 
 | ||||
|     control?: FormControl; | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     text = ''; | ||||
|     isSent = false; | ||||
|     loaded = false; | ||||
| 
 | ||||
|     protected element: HTMLElement; | ||||
| 
 | ||||
|     constructor( | ||||
|         element: ElementRef, | ||||
|         protected fb: FormBuilder, | ||||
|     ) { | ||||
|         super(); | ||||
|         this.element = element.nativeElement; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         try { | ||||
|             this.text = await this.getText(); | ||||
| 
 | ||||
|             if (!this.canEdit && !this.edit) { | ||||
|                 // User cannot edit the comment. Show it full when clicked.
 | ||||
|                 this.element.addEventListener('click', (e) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
| 
 | ||||
|                     if (this.text) { | ||||
|                         // Open a new state with the text.
 | ||||
|                         CoreTextUtils.instance.viewText(this.plugin.name, this.text, { | ||||
|                             component: this.component, | ||||
|                             componentId: this.assign.cmid, | ||||
|                             filter: true, | ||||
|                             contextLevel: 'module', | ||||
|                             instanceId: this.assign.cmid, | ||||
|                             courseId: this.assign.course, | ||||
|                         }); | ||||
|                     } | ||||
|                 }); | ||||
|             } else if (this.edit) { | ||||
|                 this.control = this.fb.control(this.text); | ||||
|             } | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Edit the comment. | ||||
|      */ | ||||
|     async editComment(): Promise<void> { | ||||
|         try { | ||||
|             const inputData = await this.editFeedback(); | ||||
|             const text = AddonModAssignFeedbackCommentsHandler.instance.getTextFromInputData(this.plugin, inputData); | ||||
| 
 | ||||
|             // Update the text and save it as draft.
 | ||||
|             this.isSent = false; | ||||
|             this.text = this.replacePluginfileUrls(text); | ||||
|             AddonModAssignFeedbackDelegate.instance.saveFeedbackDraft(this.assign.id, this.userId, this.plugin, { | ||||
|                 text: text, | ||||
|                 format: 1, | ||||
|             }); | ||||
|         } catch { | ||||
|             // User cancelled, nothing to do.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the text for the plugin. | ||||
|      * | ||||
|      * @return Promise resolved with the text. | ||||
|      */ | ||||
|     protected async getText(): Promise<string> { | ||||
|         // Check if the user already modified the comment.
 | ||||
|         const draft: AddonModAssignFeedbackCommentsDraftData | undefined = | ||||
|             await AddonModAssignFeedbackDelegate.instance.getPluginDraftData(this.assign.id, this.userId, this.plugin); | ||||
| 
 | ||||
|         if (draft) { | ||||
|             this.isSent = false; | ||||
| 
 | ||||
|             return this.replacePluginfileUrls(draft.text); | ||||
|         } | ||||
| 
 | ||||
|         // There is no draft saved. Check if we have anything offline.
 | ||||
|         const offlineData = await CoreUtils.instance.ignoreErrors( | ||||
|             AddonModAssignOffline.instance.getSubmissionGrade(this.assign.id, this.userId), | ||||
|             undefined, | ||||
|         ); | ||||
| 
 | ||||
|         if (offlineData && offlineData.plugindata && offlineData.plugindata.assignfeedbackcomments_editor) { | ||||
|             const pluginData = <AddonModAssignFeedbackCommentsPluginData>offlineData.plugindata; | ||||
| 
 | ||||
|             // Save offline as draft.
 | ||||
|             this.isSent = false; | ||||
|             AddonModAssignFeedbackDelegate.instance.saveFeedbackDraft( | ||||
|                 this.assign.id, | ||||
|                 this.userId, | ||||
|                 this.plugin, | ||||
|                 pluginData.assignfeedbackcomments_editor, | ||||
|             ); | ||||
| 
 | ||||
|             return this.replacePluginfileUrls(pluginData.assignfeedbackcomments_editor.text); | ||||
|         } | ||||
| 
 | ||||
|         // No offline data found, return online text.
 | ||||
|         this.isSent = true; | ||||
| 
 | ||||
|         return AddonModAssign.instance.getSubmissionPluginText(this.plugin); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Replace @@PLUGINFILE@@ wildcards with the real URL of embedded files. | ||||
|      * | ||||
|      * @param Text to treat. | ||||
|      * @return Treated text. | ||||
|      */ | ||||
|     replacePluginfileUrls(text: string): string { | ||||
|         const files = this.plugin.fileareas && this.plugin.fileareas[0] && this.plugin.fileareas[0].files; | ||||
| 
 | ||||
|         return CoreTextUtils.instance.replacePluginfileUrls(text, files || []); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/feedback/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/feedback/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| { | ||||
|     "pluginname": "Feedback comments" | ||||
| } | ||||
							
								
								
									
										268
									
								
								src/addons/mod/assign/feedback/comments/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								src/addons/mod/assign/feedback/comments/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,268 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { | ||||
|     AddonModAssignPlugin, | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssign, | ||||
|     AddonModAssignSavePluginData, | ||||
| } from '@addons/mod/assign/services/assign'; | ||||
| import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline'; | ||||
| import { AddonModAssignFeedbackHandler } from '@addons/mod/assign/services/feedback-delegate'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonModAssignFeedbackCommentsComponent } from '../component/comments'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for comments feedback plugin. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' }) | ||||
| export class AddonModAssignFeedbackCommentsHandlerService implements AddonModAssignFeedbackHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignFeedbackCommentsHandler'; | ||||
|     type = 'comments'; | ||||
| 
 | ||||
|     // Store the data in this service so it isn't lost if the user performs a PTR in the page.
 | ||||
|     protected drafts: { [draftId: string]: AddonModAssignFeedbackCommentsDraftData } = {}; | ||||
| 
 | ||||
|     /** | ||||
|      * Get the text to submit. | ||||
|      * | ||||
|      * @param textUtils Text utils instance. | ||||
|      * @param plugin Plugin. | ||||
|      * @param inputData Data entered in the feedback edit form. | ||||
|      * @return Text to submit. | ||||
|      */ | ||||
|     getTextFromInputData(plugin: AddonModAssignPlugin, inputData: AddonModAssignFeedbackCommentsTextData): string { | ||||
|         const files = plugin.fileareas && plugin.fileareas[0] ? plugin.fileareas[0].files : []; | ||||
| 
 | ||||
|         // The input data can have a string or an object with text and format. Get the text.
 | ||||
|         const text = inputData.assignfeedbackcomments_editor || ''; | ||||
| 
 | ||||
|         return CoreTextUtils.instance.restorePluginfileUrls(text, files || []); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Discard the draft data of the feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     discardDraft(assignId: number, userId: number, siteId?: string): void { | ||||
|         const id = this.getDraftId(assignId, userId, siteId); | ||||
|         if (typeof this.drafts[id] != 'undefined') { | ||||
|             delete this.drafts[id]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the plugin data. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @return The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent(): Type<unknown> { | ||||
|         return AddonModAssignFeedbackCommentsComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the draft saved data of the feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Data (or promise resolved with the data). | ||||
|      */ | ||||
|     getDraft(assignId: number, userId: number, siteId?: string): AddonModAssignFeedbackCommentsDraftData | undefined { | ||||
|         const id = this.getDraftId(assignId, userId, siteId); | ||||
| 
 | ||||
|         if (typeof this.drafts[id] != 'undefined') { | ||||
|             return this.drafts[id]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a draft ID. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Draft ID. | ||||
|      */ | ||||
|     protected getDraftId(assignId: number, userId: number, siteId?: string): string { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         return siteId + '#' + assignId + '#' + userId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): CoreWSExternalFile[] { | ||||
|         return AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the feedback data has changed for this plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the feedback. | ||||
|      * @param userId User ID of the submission. | ||||
|      * @return Boolean (or promise resolved with boolean): whether the data has changed. | ||||
|      */ | ||||
|     async hasDataChanged( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: AddonModAssignFeedbackCommentsTextData, | ||||
|         userId: number, | ||||
|     ): Promise<boolean> { | ||||
|         // Get it from plugin or offline.
 | ||||
|         const offlineData = await CoreUtils.instance.ignoreErrors( | ||||
|             AddonModAssignOffline.instance.getSubmissionGrade(assign.id, userId), | ||||
|             undefined, | ||||
|         ); | ||||
| 
 | ||||
|         if (offlineData?.plugindata?.assignfeedbackcomments_editor) { | ||||
|             const pluginData = <AddonModAssignFeedbackCommentsPluginData>offlineData.plugindata; | ||||
| 
 | ||||
|             return !!pluginData.assignfeedbackcomments_editor.text; | ||||
|         } | ||||
| 
 | ||||
|         // No offline data found, get text from plugin.
 | ||||
|         const initialText = AddonModAssign.instance.getSubmissionPluginText(plugin); | ||||
|         const newText = AddonModAssignFeedbackCommentsHandler.instance.getTextFromInputData(plugin, inputData); | ||||
| 
 | ||||
|         if (typeof newText == 'undefined') { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // Check if text has changed.
 | ||||
|         return initialText != newText; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the plugin has draft data stored. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Boolean or promise resolved with boolean: whether the plugin has draft data. | ||||
|      */ | ||||
|     hasDraftData(assignId: number, userId: number, siteId?: string): boolean | Promise<boolean> { | ||||
|         const draft = this.getDraft(assignId, userId, siteId); | ||||
| 
 | ||||
|         return !!draft; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         // In here we should check if comments is not disabled in site.
 | ||||
|         // But due to this is not a common comments place and it can be disabled separately into Moodle (disabling the plugin).
 | ||||
|         // We are leaving it always enabled. It's also a teacher's feature.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the draft data saved. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     prepareFeedbackData( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         siteId?: string, | ||||
|     ): void { | ||||
| 
 | ||||
|         const draft = this.getDraft(assignId, userId, siteId); | ||||
| 
 | ||||
|         if (draft) { | ||||
|             // Add some HTML to the text if needed.
 | ||||
|             draft.text = CoreTextUtils.instance.formatHtmlLines(draft.text); | ||||
| 
 | ||||
|             pluginData.assignfeedbackcomments_editor = draft; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save draft data of the feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param data The data to save. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     saveDraft( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         data: AddonModAssignFeedbackCommentsDraftData, | ||||
|         siteId?: string, | ||||
|     ): void { | ||||
| 
 | ||||
|         if (data) { | ||||
|             this.drafts[this.getDraftId(assignId, userId, siteId)] = data; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignFeedbackCommentsHandler = makeSingleton(AddonModAssignFeedbackCommentsHandlerService); | ||||
| 
 | ||||
| export type AddonModAssignFeedbackCommentsTextData = { | ||||
|     // The text for this submission.
 | ||||
|     assignfeedbackcomments_editor: string; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
| }; | ||||
| 
 | ||||
| export type AddonModAssignFeedbackCommentsDraftData = { | ||||
|     text: string; // The text for this feedback.
 | ||||
|     format: number; // The format for this feedback.
 | ||||
| }; | ||||
| 
 | ||||
| export type AddonModAssignFeedbackCommentsPluginData = { | ||||
|     // Editor structure.
 | ||||
|     // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
|     assignfeedbackcomments_editor: AddonModAssignFeedbackCommentsDraftData; | ||||
| }; | ||||
| @ -0,0 +1,11 @@ | ||||
| <!-- Read only. --> | ||||
| <ion-item class="ion-text-wrap" *ngIf="files && files.length"> | ||||
|     <ion-label> | ||||
|         <h2>{{plugin.name}}</h2> | ||||
|         <ng-container> | ||||
|             <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" | ||||
|                 [alwaysDownload]="true"> | ||||
|             </core-file> | ||||
|         </ng-container> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
							
								
								
									
										41
									
								
								src/addons/mod/assign/feedback/editpdf/component/editpdf.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/addons/mod/assign/feedback/editpdf/component/editpdf.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AddonModAssignFeedbackPluginComponent } from '@addons/mod/assign/components/feedback-plugin/feedback-plugin'; | ||||
| import { AddonModAssignProvider, AddonModAssign } from '@addons/mod/assign/services/assign'; | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a edit pdf feedback plugin. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-feedback-edit-pdf', | ||||
|     templateUrl: 'addon-mod-assign-feedback-editpdf.html', | ||||
| }) | ||||
| export class AddonModAssignFeedbackEditPdfComponent extends AddonModAssignFeedbackPluginComponent implements OnInit { | ||||
| 
 | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     files: CoreWSExternalFile[] = []; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         if (this.plugin) { | ||||
|             this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										45
									
								
								src/addons/mod/assign/feedback/editpdf/editpdf.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addons/mod/assign/feedback/editpdf/editpdf.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { AddonModAssignFeedbackEditPdfHandler } from './services/handler'; | ||||
| import { AddonModAssignFeedbackEditPdfComponent } from './component/editpdf'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModAssignFeedbackEditPdfComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonModAssignFeedbackDelegate.instance.registerHandler(AddonModAssignFeedbackEditPdfHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModAssignFeedbackEditPdfComponent, | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonModAssignFeedbackEditPdfComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignFeedbackEditPdfModule {} | ||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/feedback/editpdf/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/feedback/editpdf/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| { | ||||
|     "pluginname": "Annotate PDF" | ||||
| } | ||||
							
								
								
									
										73
									
								
								src/addons/mod/assign/feedback/editpdf/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/addons/mod/assign/feedback/editpdf/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { | ||||
|     AddonModAssignPlugin, | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssign, | ||||
| } from '@addons/mod/assign/services/assign'; | ||||
| import { AddonModAssignFeedbackHandler } from '@addons/mod/assign/services/feedback-delegate'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonModAssignFeedbackEditPdfComponent } from '../component/editpdf'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for edit pdf feedback plugin. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' }) | ||||
| export class AddonModAssignFeedbackEditPdfHandlerService implements AddonModAssignFeedbackHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignFeedbackEditPdfHandler'; | ||||
|     type = 'editpdf'; | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the plugin data. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @return The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent(): Type<unknown> { | ||||
|         return AddonModAssignFeedbackEditPdfComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): CoreWSExternalFile[] { | ||||
|         return AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignFeedbackEditPdfHandler = makeSingleton(AddonModAssignFeedbackEditPdfHandlerService); | ||||
							
								
								
									
										27
									
								
								src/addons/mod/assign/feedback/feedback.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/addons/mod/assign/feedback/feedback.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { AddonModAssignFeedbackCommentsModule } from './comments/comments.module'; | ||||
| import { AddonModAssignFeedbackEditPdfModule } from './editpdf/editpdf.module'; | ||||
| import { AddonModAssignFeedbackFileModule } from './file/file.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         AddonModAssignFeedbackCommentsModule, | ||||
|         AddonModAssignFeedbackEditPdfModule, | ||||
|         AddonModAssignFeedbackFileModule, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignFeedbackModule { } | ||||
| @ -0,0 +1,11 @@ | ||||
| <!-- Read only. --> | ||||
| <ion-item class="ion-text-wrap" *ngIf="files && files.length"> | ||||
|     <ion-label> | ||||
|         <h2>{{plugin.name}}</h2> | ||||
|         <ng-container> | ||||
|             <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" | ||||
|                 [alwaysDownload]="true"> | ||||
|             </core-file> | ||||
|         </ng-container> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
							
								
								
									
										41
									
								
								src/addons/mod/assign/feedback/file/component/file.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/addons/mod/assign/feedback/file/component/file.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AddonModAssignFeedbackPluginComponent } from '@addons/mod/assign/components/feedback-plugin/feedback-plugin'; | ||||
| import { AddonModAssign, AddonModAssignProvider } from '@addons/mod/assign/services/assign'; | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a file feedback plugin. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-feedback-file', | ||||
|     templateUrl: 'addon-mod-assign-feedback-file.html', | ||||
| }) | ||||
| export class AddonModAssignFeedbackFileComponent extends AddonModAssignFeedbackPluginComponent implements OnInit { | ||||
| 
 | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     files: CoreWSExternalFile[] = []; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         if (this.plugin) { | ||||
|             this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										45
									
								
								src/addons/mod/assign/feedback/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addons/mod/assign/feedback/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { AddonModAssignFeedbackFileHandler } from './services/handler'; | ||||
| import { AddonModAssignFeedbackFileComponent } from './component/file'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModAssignFeedbackFileComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonModAssignFeedbackDelegate.instance.registerHandler(AddonModAssignFeedbackFileHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModAssignFeedbackFileComponent, | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonModAssignFeedbackFileComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignFeedbackFileModule {} | ||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/feedback/file/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/feedback/file/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| { | ||||
|     "pluginname": "File feedback" | ||||
| } | ||||
							
								
								
									
										73
									
								
								src/addons/mod/assign/feedback/file/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/addons/mod/assign/feedback/file/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { | ||||
|     AddonModAssignPlugin, | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssign, | ||||
| } from '@addons/mod/assign/services/assign'; | ||||
| import { AddonModAssignFeedbackHandler } from '@addons/mod/assign/services/feedback-delegate'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonModAssignFeedbackFileComponent } from '../component/file'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for file feedback plugin. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' }) | ||||
| export class AddonModAssignFeedbackFileHandlerService implements AddonModAssignFeedbackHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignFeedbackFileHandler'; | ||||
|     type = 'file'; | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the plugin data. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @return The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent(): Type<unknown> { | ||||
|         return AddonModAssignFeedbackFileComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): CoreWSExternalFile[] { | ||||
|         return AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignFeedbackFileHandler = makeSingleton(AddonModAssignFeedbackFileHandlerService); | ||||
							
								
								
									
										104
									
								
								src/addons/mod/assign/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/addons/mod/assign/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,104 @@ | ||||
| { | ||||
|     "acceptsubmissionstatement": "Please accept the submission statement.", | ||||
|     "addattempt": "Allow another attempt", | ||||
|     "addnewattempt": "Add a new attempt", | ||||
|     "addnewattemptfromprevious": "Add a new attempt based on previous submission", | ||||
|     "addsubmission": "Add submission", | ||||
|     "allowsubmissionsfromdate": "Allow submissions from", | ||||
|     "allowsubmissionsfromdatesummary": "This assignment will accept submissions from <strong>{{$a}}</strong>", | ||||
|     "allowsubmissionsanddescriptionfromdatesummary": "The assignment details and submission form will be available from <strong>{{$a}}</strong>", | ||||
|     "applytoteam": "Apply grades and feedback to entire group", | ||||
|     "assignmentisdue": "Assignment is due", | ||||
|     "attemptnumber": "Attempt number", | ||||
|     "attemptreopenmethod": "Attempts reopened", | ||||
|     "attemptreopenmethod_manual": "Manually", | ||||
|     "attemptreopenmethod_untilpass": "Automatically until pass", | ||||
|     "attemptsettings": "Attempt settings", | ||||
|     "cannotgradefromapp": "Certain grading methods are not yet supported by the app and cannot be modified.", | ||||
|     "cannoteditduetostatementsubmission": "You can't add or edit a submission in the app because the submission statement could not be retrieved from the site.", | ||||
|     "cannotsubmitduetostatementsubmission": "You can't make a submission in the app because the submission statement could not be retrieved from the site.", | ||||
|     "confirmsubmission": "Are you sure you want to submit your work for grading? You will not be able to make any more changes.", | ||||
|     "currentgrade": "Current grade in gradebook", | ||||
|     "cutoffdate": "Cut-off date", | ||||
|     "currentattempt": "This is attempt {{$a}}.", | ||||
|     "currentattemptof": "This is attempt {{$a.attemptnumber}} ( {{$a.maxattempts}} attempts allowed ).", | ||||
|     "defaultteam": "Default group", | ||||
|     "duedate": "Due date", | ||||
|     "duedateno": "No due date", | ||||
|     "duedatereached": "The due date for this assignment has now passed", | ||||
|     "editingstatus": "Editing status", | ||||
|     "editsubmission": "Edit submission", | ||||
|     "erroreditpluginsnotsupported": "You can't add or edit a submission in the app because certain plugins are not yet supported for editing.", | ||||
|     "errorshowinginformation": "Submission information cannot be displayed.", | ||||
|     "extensionduedate": "Extension due date", | ||||
|     "feedbacknotsupported": "This feedback is not supported by the app and may not contain all the information.", | ||||
|     "grade": "Grade", | ||||
|     "graded": "Graded", | ||||
|     "gradedby": "Graded by", | ||||
|     "gradedfollowupsubmit": "Graded - follow up submission received", | ||||
|     "gradenotsynced": "Grade not synced", | ||||
|     "gradedon": "Graded on", | ||||
|     "gradelocked": "This grade is locked or overridden in the gradebook.", | ||||
|     "gradeoutof": "Grade out of {{$a}}", | ||||
|     "gradingstatus": "Grading status", | ||||
|     "groupsubmissionsettings": "Group submission settings", | ||||
|     "hiddenuser": "Participant", | ||||
|     "latesubmissions": "Late submissions", | ||||
|     "latesubmissionsaccepted": "Allowed until {{$a}}", | ||||
|     "markingworkflowstate": "Marking workflow state", | ||||
|     "markingworkflowstateinmarking": "In marking", | ||||
|     "markingworkflowstateinreview": "In review", | ||||
|     "markingworkflowstatenotmarked": "Not marked", | ||||
|     "markingworkflowstatereadyforreview": "Marking completed", | ||||
|     "markingworkflowstatereadyforrelease": "Ready for release", | ||||
|     "markingworkflowstatereleased": "Released", | ||||
|     "modulenameplural": "Assignments", | ||||
|     "multipleteams": "Member of more than one group", | ||||
|     "multipleteams_desc": "The assignment requires submission in groups. You are a member of more than one group. To be able to submit you must be a member of only one group. Please contact your teacher to change your group membership.", | ||||
|     "noattempt": "No attempt", | ||||
|     "nomoresubmissionsaccepted": "Only allowed for participants who have been granted an extension", | ||||
|     "noonlinesubmissions": "This assignment does not require you to submit anything online", | ||||
|     "nosubmission": "Nothing has been submitted for this assignment", | ||||
|     "notallparticipantsareshown": "Participants who have not made a submission are not shown.", | ||||
|     "noteam": "Not a member of any group", | ||||
|     "noteam_desc": "This assignment requires submission in groups. You are not a member of any group, so you cannot create a submission. Please contact your teacher to be added to a group.", | ||||
|     "notgraded": "Not graded", | ||||
|     "numberofdraftsubmissions": "Drafts", | ||||
|     "numberofparticipants": "Participants", | ||||
|     "numberofsubmittedassignments": "Submitted", | ||||
|     "numberofsubmissionsneedgrading": "Needs grading", | ||||
|     "numberofteams": "Groups", | ||||
|     "numwords": "{{$a}} words", | ||||
|     "outof": "{{$a.current}} out of {{$a.total}}", | ||||
|     "overdue": "<font color=\"red\">Assignment is overdue by: {{$a}}</font>", | ||||
|     "submissioneditable": "Student can edit this submission", | ||||
|     "submissionnoteditable": "Student cannot edit this submission", | ||||
|     "submissionnotsupported": "This submission is not supported by the app and may not contain all the information.", | ||||
|     "submission": "Submission", | ||||
|     "submissionslocked": "This assignment is not accepting submissions", | ||||
|     "submissionstatus_draft": "Draft (not submitted)", | ||||
|     "submissionstatusheading": "Submission status", | ||||
|     "submissionstatus_marked": "Graded", | ||||
|     "submissionstatus_new": "No submission", | ||||
|     "submissionstatus_reopened": "Reopened", | ||||
|     "submissionstatus_submitted": "Submitted for grading", | ||||
|     "submissionstatus_": "No submission", | ||||
|     "submissionstatus": "Submission status", | ||||
|     "submissionteam": "Group", | ||||
|     "submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.", | ||||
|     "submitassignment": "Submit assignment", | ||||
|     "submittedearly": "Assignment was submitted {{$a}} early", | ||||
|     "submittedlate": "Assignment was submitted {{$a}} late", | ||||
|     "syncblockedusercomponent": "user grade", | ||||
|     "timemodified": "Last modified", | ||||
|     "timeremaining": "Time remaining", | ||||
|     "ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.", | ||||
|     "ungroupedusersoptional": "The setting 'Students submit in groups' is enabled and some users are either not a member of any group, or are a member of more than one group. Please be aware that these students will submit as members of the 'Default group'.", | ||||
|     "unlimitedattempts": "Unlimited", | ||||
|     "userwithid": "User with ID {{id}}", | ||||
|     "userswhoneedtosubmit": "Users who need to submit: {{$a}}", | ||||
|     "viewsubmission": "View submission", | ||||
|     "warningsubmissionmodified": "The user submission was modified on the site.", | ||||
|     "warningsubmissiongrademodified": "The submission grade was modified on the site.", | ||||
|     "wordlimit": "Word limit" | ||||
| } | ||||
							
								
								
									
										39
									
								
								src/addons/mod/assign/pages/edit/edit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/addons/mod/assign/pages/edit/edit.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|             <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="save()" [attr.aria-label]="'core.save' | translate"> | ||||
|                 {{ 'core.save' | translate }} | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <ion-list *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length"> | ||||
|             <!-- @todo: plagiarism_print_disclosure --> | ||||
|             <form name="addon-mod_assign-edit-form" #editSubmissionForm> | ||||
|                 <!-- Submission statement. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="submissionStatement"> | ||||
|                     <ion-label> | ||||
|                         <core-format-text [text]="submissionStatement" [filter]="false"> | ||||
|                         </core-format-text> | ||||
|                     </ion-label> | ||||
|                     <ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="submissionStatementAccepted"></ion-checkbox> | ||||
|                     <!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. --> | ||||
|                     <input item-content type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement"> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <addon-mod-assign-submission-plugin *ngFor="let plugin of userSubmission.plugins" [assign]="assign" | ||||
|                     [submission]="userSubmission" [plugin]="plugin" [edit]="true" [allowOffline]="allowOffline"> | ||||
|                 </addon-mod-assign-submission-plugin> | ||||
|             </form> | ||||
|         </ion-list> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										396
									
								
								src/addons/mod/assign/pages/edit/edit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										396
									
								
								src/addons/mod/assign/pages/edit/edit.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,396 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEventActivityDataSentData, CoreEvents } from '@singletons/events'; | ||||
| import { | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssign, | ||||
|     AddonModAssignSubmissionStatusOptions, | ||||
|     AddonModAssignGetSubmissionStatusWSResponse, | ||||
|     AddonModAssignSavePluginData, | ||||
|     AddonModAssignSubmissionSavedEventData, | ||||
|     AddonModAssignSubmittedForGradingEventData, | ||||
| } from '../../services/assign'; | ||||
| import { AddonModAssignHelper } from '../../services/assign-helper'; | ||||
| import { AddonModAssignOffline } from '../../services/assign-offline'; | ||||
| import { AddonModAssignSync } from '../../services/assign-sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that allows adding or editing an assigment submission. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-assign-edit', | ||||
|     templateUrl: 'edit.html', | ||||
| }) | ||||
| export class AddonModAssignEditPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild('editSubmissionForm') formElement?: ElementRef; | ||||
| 
 | ||||
|     title: string; // Title to display.
 | ||||
|     assign?: AddonModAssignAssign; // Assignment.
 | ||||
|     courseId!: number; // Course ID the assignment belongs to.
 | ||||
|     moduleId!: number; // Module ID the submission belongs to.
 | ||||
|     userSubmission?: AddonModAssignSubmission; // The user submission.
 | ||||
|     allowOffline = false; // Whether offline is allowed.
 | ||||
|     submissionStatement?: string; // The submission statement.
 | ||||
|     submissionStatementAccepted = false; // Whether submission statement is accepted.
 | ||||
|     loaded = false; // Whether data has been loaded.
 | ||||
| 
 | ||||
|     protected userId: number; // User doing the submission.
 | ||||
|     protected isBlind = false; // Whether blind is used.
 | ||||
|     protected editText: string; // "Edit submission" translated text.
 | ||||
|     protected saveOffline = false; // Whether to save data in offline.
 | ||||
|     protected hasOffline = false; // Whether the assignment has offline data.
 | ||||
|     protected isDestroyed = false; // Whether the component has been destroyed.
 | ||||
|     protected forceLeave = false; // To allow leaving the page without checking for changes.
 | ||||
| 
 | ||||
|     constructor( | ||||
|         protected route: ActivatedRoute, | ||||
|     ) { | ||||
|         this.userId = CoreSites.instance.getCurrentSiteUserId(); // Right now we can only edit current user's submissions.
 | ||||
|         this.editText = Translate.instance.instant('addon.mod_assign.editsubmission'); | ||||
|         this.title = this.editText; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!; | ||||
|         this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!; | ||||
|         this.isBlind = !!CoreNavigator.instance.getRouteNumberParam('blindId'); | ||||
| 
 | ||||
|         this.fetchAssignment().finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we can leave the page or not. | ||||
|      * | ||||
|      * @return Resolved if we can leave it, rejected if not. | ||||
|      */ | ||||
|     async ionViewCanLeave(): Promise<void> { | ||||
|         if (this.forceLeave) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Check if data has changed.
 | ||||
|         const changed = await this.hasDataChanged(); | ||||
|         if (changed) { | ||||
|             await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit')); | ||||
|         } | ||||
| 
 | ||||
|         // Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
 | ||||
|         AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, this.getInputData()); | ||||
| 
 | ||||
|         CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch assignment data. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchAssignment(): Promise<void> { | ||||
|         const currentUserId = CoreSites.instance.getCurrentSiteUserId(); | ||||
| 
 | ||||
|         try { | ||||
|             // Get assignment data.
 | ||||
|             this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); | ||||
|             this.title = this.assign.name || this.title; | ||||
| 
 | ||||
|             if (!this.isDestroyed) { | ||||
|                 // Block the assignment.
 | ||||
|                 CoreSync.instance.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); | ||||
|             } | ||||
| 
 | ||||
|             // Wait for sync to be over (if any).
 | ||||
|             await AddonModAssignSync.instance.waitForSync(this.assign.id); | ||||
| 
 | ||||
|             // Get submission status. Ignore cache to get the latest data.
 | ||||
|             const options: AddonModAssignSubmissionStatusOptions = { | ||||
|                 userId: this.userId, | ||||
|                 isBlind: this.isBlind, | ||||
|                 cmId: this.assign.cmid, | ||||
|                 filter: false, | ||||
|                 readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|             }; | ||||
| 
 | ||||
|             let submissionStatus: AddonModAssignGetSubmissionStatusWSResponse; | ||||
|             try { | ||||
|                 submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options); | ||||
|                 this.userSubmission = | ||||
|                     AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt); | ||||
|             } catch (error) { | ||||
|                 // Cannot connect. Get cached data.
 | ||||
|                 options.filter = true; | ||||
|                 options.readingStrategy = CoreSitesReadingStrategy.PreferCache; | ||||
| 
 | ||||
|                 submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options); | ||||
|                 this.userSubmission = | ||||
|                     AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt); | ||||
| 
 | ||||
|                 // Check if the user can edit it in offline.
 | ||||
|                 const canEditOffline = | ||||
|                     await AddonModAssignHelper.instance.canEditSubmissionOffline(this.assign, this.userSubmission); | ||||
|                 if (!canEditOffline) { | ||||
|                     // Submission cannot be edited in offline, reject.
 | ||||
|                     this.allowOffline = false; | ||||
|                     throw error; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (!submissionStatus.lastattempt?.canedit) { | ||||
|                 // Can't edit. Reject.
 | ||||
|                 throw new CoreError(Translate.instance.instant('core.nopermissions', { $a: this.editText })); | ||||
|             } | ||||
| 
 | ||||
|             this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point.
 | ||||
|             // Only show submission statement if we are editing our own submission.
 | ||||
|             if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) { | ||||
|                 this.submissionStatement = this.assign.submissionstatement; | ||||
|             } else { | ||||
|                 this.submissionStatement = undefined; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 // Check if there's any offline data for this submission.
 | ||||
|                 const offlineData = await AddonModAssignOffline.instance.getSubmission(this.assign.id, this.userId); | ||||
| 
 | ||||
|                 this.hasOffline = offlineData?.plugindata && Object.keys(offlineData.plugindata).length > 0; | ||||
|             } catch { | ||||
|                 // No offline data found.
 | ||||
|                 this.hasOffline = false; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting assigment data.'); | ||||
| 
 | ||||
|             // Leave the player.
 | ||||
|             this.leaveWithoutCheck(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the input data. | ||||
|      * | ||||
|      * @return Input data. | ||||
|      */ | ||||
|     protected getInputData(): Record<string, unknown> { | ||||
|         return CoreDomUtils.instance.getDataFromForm(document.forms['addon-mod_assign-edit-form']); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if data has changed. | ||||
|      * | ||||
|      * @return Promise resolved with boolean: whether data has changed. | ||||
|      */ | ||||
|     protected async hasDataChanged(): Promise<boolean> { | ||||
|         // Usually the hasSubmissionDataChanged call will be resolved inmediately, causing the modal to be shown just an instant.
 | ||||
|         // We'll wait a bit before showing it to prevent this "blink".
 | ||||
|         const modal = await CoreDomUtils.instance.showModalLoading(); | ||||
| 
 | ||||
|         const data = this.getInputData(); | ||||
| 
 | ||||
|         return AddonModAssignHelper.instance.hasSubmissionDataChanged(this.assign!, this.userSubmission, data).finally(() => { | ||||
|             modal.dismiss(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Leave the view without checking for changes. | ||||
|      */ | ||||
|     protected leaveWithoutCheck(): void { | ||||
|         this.forceLeave = true; | ||||
|         CoreNavigator.instance.back(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get data to submit based on the input data. | ||||
|      * | ||||
|      * @param inputData The input data. | ||||
|      * @return Promise resolved with the data to submit. | ||||
|      */ | ||||
|     protected prepareSubmissionData(inputData: Record<string, unknown>): Promise<AddonModAssignSavePluginData> { | ||||
|         // If there's offline data, always save it in offline.
 | ||||
|         this.saveOffline = this.hasOffline; | ||||
| 
 | ||||
|         try { | ||||
|             return AddonModAssignHelper.instance.prepareSubmissionPluginData( | ||||
|                 this.assign!, | ||||
|                 this.userSubmission, | ||||
|                 inputData, | ||||
|                 this.hasOffline, | ||||
|             ); | ||||
|         } catch (error) { | ||||
|             if (this.allowOffline && !this.saveOffline) { | ||||
|                 // Cannot submit in online, prepare for offline usage.
 | ||||
|                 this.saveOffline = true; | ||||
| 
 | ||||
|                 return AddonModAssignHelper.instance.prepareSubmissionPluginData( | ||||
|                     this.assign!, | ||||
|                     this.userSubmission, | ||||
|                     inputData, | ||||
|                     true, | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the submission. | ||||
|      */ | ||||
|     async save(): Promise<void> { | ||||
|         // Check if data has changed.
 | ||||
|         const changed = await this.hasDataChanged(); | ||||
|         if (!changed) { | ||||
|             // Nothing to save, just go back.
 | ||||
|             this.leaveWithoutCheck(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|         try { | ||||
|             await this.saveSubmission(); | ||||
|             this.leaveWithoutCheck(); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'Error saving submission.'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the submission. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async saveSubmission(): Promise<void> { | ||||
|         const inputData = this.getInputData(); | ||||
| 
 | ||||
|         if (this.submissionStatement && (!inputData.submissionstatement || inputData.submissionstatement === 'false')) { | ||||
|             throw Translate.instance.instant('addon.mod_assign.acceptsubmissionstatement'); | ||||
|         } | ||||
| 
 | ||||
|         let modal = await CoreDomUtils.instance.showModalLoading(); | ||||
|         let size = -1; | ||||
| 
 | ||||
|         // Get size to ask for confirmation.
 | ||||
|         try { | ||||
|             size = await AddonModAssignHelper.instance.getSubmissionSizeForEdit(this.assign!, this.userSubmission!, inputData); | ||||
|         } catch (error) { | ||||
|             // Error calculating size, return -1.
 | ||||
|             size = -1; | ||||
|         } | ||||
| 
 | ||||
|         modal.dismiss(); | ||||
| 
 | ||||
|         try { | ||||
|             // Confirm action.
 | ||||
|             await CoreFileUploaderHelper.instance.confirmUploadFile(size, true, this.allowOffline); | ||||
| 
 | ||||
|             modal = await CoreDomUtils.instance.showModalLoading('core.sending', true); | ||||
| 
 | ||||
|             const pluginData = await this.prepareSubmissionData(inputData); | ||||
|             if (!Object.keys(pluginData).length) { | ||||
|                 // Nothing to save.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             let sent: boolean; | ||||
| 
 | ||||
|             if (this.saveOffline) { | ||||
|                 // Save submission in offline.
 | ||||
|                 sent = false; | ||||
|                 await AddonModAssignOffline.instance.saveSubmission( | ||||
|                     this.assign!.id, | ||||
|                     this.courseId, | ||||
|                     pluginData, | ||||
|                     this.userSubmission!.timemodified, | ||||
|                     !this.assign!.submissiondrafts, | ||||
|                     this.userId, | ||||
|                 ); | ||||
|             } else { | ||||
|                 // Try to send it to server.
 | ||||
|                 sent = await AddonModAssign.instance.saveSubmission( | ||||
|                     this.assign!.id, | ||||
|                     this.courseId, | ||||
|                     pluginData, | ||||
|                     this.allowOffline, | ||||
|                     this.userSubmission!.timemodified, | ||||
|                     !!this.assign!.submissiondrafts, | ||||
|                     this.userId, | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             // Clear temporary data from plugins.
 | ||||
|             AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, inputData); | ||||
| 
 | ||||
|             if (sent) { | ||||
|                 CoreEvents.trigger<CoreEventActivityDataSentData>(CoreEvents.ACTIVITY_DATA_SENT, { module: 'assign' }); | ||||
|             } | ||||
| 
 | ||||
|             // Submission saved, trigger events.
 | ||||
|             CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.instance.getCurrentSiteId()); | ||||
| 
 | ||||
|             CoreEvents.trigger<AddonModAssignSubmissionSavedEventData>( | ||||
|                 AddonModAssignProvider.SUBMISSION_SAVED_EVENT, | ||||
|                 { | ||||
|                     assignmentId: this.assign!.id, | ||||
|                     submissionId: this.userSubmission!.id, | ||||
|                     userId: this.userId, | ||||
|                 }, | ||||
|                 CoreSites.instance.getCurrentSiteId(), | ||||
|             ); | ||||
| 
 | ||||
|             if (!this.assign!.submissiondrafts) { | ||||
|                 // No drafts allowed, so it was submitted. Trigger event.
 | ||||
|                 CoreEvents.trigger<AddonModAssignSubmittedForGradingEventData>( | ||||
|                     AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, | ||||
|                     { | ||||
|                         assignmentId: this.assign!.id, | ||||
|                         submissionId: this.userSubmission!.id, | ||||
|                         userId: this.userId, | ||||
|                     }, | ||||
|                     CoreSites.instance.getCurrentSiteId(), | ||||
|                 ); | ||||
|             } | ||||
|         } finally { | ||||
|             modal.dismiss(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
| 
 | ||||
|         // Unblock the assignment.
 | ||||
|         if (this.assign) { | ||||
|             CoreSync.instance.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										22
									
								
								src/addons/mod/assign/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/addons/mod/assign/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|             <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </ion-title> | ||||
| 
 | ||||
|         <ion-buttons slot="end"> | ||||
|             <!-- The buttons defined by the component will be added in here. --> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-refresher slot="fixed" [disabled]="!assignComponent?.loaded" (ionRefresh)="assignComponent?.doRefresh($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
| 
 | ||||
|     <addon-mod-assign-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-assign-index> | ||||
| </ion-content> | ||||
							
								
								
									
										68
									
								
								src/addons/mod/assign/pages/index/index.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/addons/mod/assign/pages/index/index.page.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, ViewChild } from '@angular/core'; | ||||
| import { CoreCourseWSModule } from '@features/course/services/course'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { AddonModAssignIndexComponent } from '../../components/index/index'; | ||||
| import { AddonModAssignAssign } from '../../services/assign'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays an assign. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-assign-index', | ||||
|     templateUrl: 'index.html', | ||||
| }) | ||||
| export class AddonModAssignIndexPage implements OnInit { | ||||
| 
 | ||||
|     @ViewChild(AddonModAssignIndexComponent) assignComponent?: AddonModAssignIndexComponent; | ||||
| 
 | ||||
|     title?: string; | ||||
|     module?: CoreCourseWSModule; | ||||
|     courseId?: number; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.module = CoreNavigator.instance.getRouteParam('module'); | ||||
|         this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId'); | ||||
|         this.title = this.module?.name; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update some data based on the assign instance. | ||||
|      * | ||||
|      * @param assign Assign instance. | ||||
|      */ | ||||
|     updateData(assign: AddonModAssignAssign): void { | ||||
|         this.title = assign.name || this.title; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User entered the page. | ||||
|      */ | ||||
|     ionViewDidEnter(): void { | ||||
|         this.assignComponent?.ionViewDidEnter(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User left the page. | ||||
|      */ | ||||
|     ionViewDidLeave(): void { | ||||
|         this.assignComponent?.ionViewDidLeave(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,85 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|             <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId"  [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </ion-title> | ||||
| 
 | ||||
|         <ion-buttons slot="end"></ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| 
 | ||||
| <ion-content> | ||||
|     <core-split-view> | ||||
|         <ion-refresher slot="fixed" [disabled]="!loaded || !submissions.loaded" (ionRefresh)="refreshList($event)"> | ||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|         </ion-refresher> | ||||
|         <core-loading [hideUntil]="loaded && submissions.loaded"> | ||||
|             <core-empty-box *ngIf="!submissions || submissions.empty" icon="fas-file-signature" | ||||
|                 [message]="'addon.mod_assign.submissionstatus_' | translate"> | ||||
|             </core-empty-box> | ||||
| 
 | ||||
|             <ion-list> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="(groupInfo.separateGroups || groupInfo.visibleGroups)"> | ||||
|                     <ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.separateGroups"> | ||||
|                         {{ 'core.groupsseparate' | translate }} | ||||
|                     </ion-label> | ||||
|                     <ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.visibleGroups"> | ||||
|                         {{ 'core.groupsvisible' | translate }} | ||||
|                     </ion-label> | ||||
|                     <ion-select [(ngModel)]="groupId" (ionChange)="setGroup(groupId)" aria-labelledby="addon-assign-groupslabel" | ||||
|                         interface="action-sheet" slot="end"> | ||||
|                         <ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id"> | ||||
|                             {{groupOpt.name}} | ||||
|                         </ion-select-option> | ||||
|                     </ion-select> | ||||
|                 </ion-item> | ||||
|                 <!-- List of submissions. --> | ||||
|                 <ng-container *ngFor="let submission of submissions.items"> | ||||
|                     <ion-item class="ion-text-wrap" (click)="submissions.select(submission)" | ||||
|                         [class.core-selected-item]="submissions.isSelected(submission)"> | ||||
|                         <core-user-avatar [user]="submission" [linkProfile]="false" slot="start"></core-user-avatar> | ||||
|                         <ion-label> | ||||
|                             <h2 *ngIf="submission.userfullname">{{submission.userfullname}}</h2> | ||||
|                             <h2 *ngIf="!submission.userfullname"> | ||||
|                                 {{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}} | ||||
|                             </h2> | ||||
|                             <p *ngIf="assign && assign!.teamsubmission"> | ||||
|                                 <span *ngIf="submission.groupname">{{submission.groupname}}</span> | ||||
|                                 <span *ngIf="assign!.preventsubmissionnotingroup && !submission.groupname && submission.noGroups | ||||
|                                     && !submission.blindid" class="text-danger"> | ||||
|                                     {{ 'addon.mod_assign.noteam' | translate }} | ||||
|                                 </span> | ||||
|                                 <span *ngIf="assign!.preventsubmissionnotingroup && !submission.groupname && submission.manyGroups | ||||
|                                     && !submission.blindid" class="text-danger"> | ||||
|                                     {{ 'addon.mod_assign.multipleteams' | translate }} | ||||
|                                 </span> | ||||
|                                 <span *ngIf="!assign!.preventsubmissionnotingroup && !submission.groupname"> | ||||
|                                     {{ 'addon.mod_assign.defaultteam' | translate }} | ||||
|                                 </span> | ||||
|                             </p> | ||||
|                             <ion-badge class="ion-text-center ion-text-wrap" [color]="submission.statusColor" | ||||
|                                 *ngIf="submission.statusTranslated"> | ||||
|                                 {{ submission.statusTranslated }} | ||||
|                             </ion-badge> | ||||
|                             <ion-badge class="ion-text-center ion-text-wrap" [color]="submission.gradingColor" | ||||
|                                 *ngIf="submission.gradingStatusTranslationId"> | ||||
|                                 {{ submission.gradingStatusTranslationId | translate }} | ||||
|                             </ion-badge> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <ion-card class="ion-text-wrap core-warning-card" *ngIf="!haveAllParticipants"> | ||||
|                     <ion-item> | ||||
|                         <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon> | ||||
|                         <ion-label>{{ 'addon.mod_assign.notallparticipantsareshown' | translate }}</ion-label> | ||||
|                     </ion-item> | ||||
|                 </ion-card> | ||||
|             </ion-list> | ||||
|         </core-loading> | ||||
|     </core-split-view> | ||||
| </ion-content> | ||||
| @ -0,0 +1,393 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnDestroy, AfterViewInit, ViewChild } from '@angular/core'; | ||||
| import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; | ||||
| import { CorePageItemsListManager } from '@classes/page-items-list-manager'; | ||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreGroupInfo, CoreGroups } from '@services/groups'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreObject } from '@singletons/object'; | ||||
| import { | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssign, | ||||
|     AddonModAssignGradedEventData, | ||||
| } from '../../services/assign'; | ||||
| import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../../services/assign-helper'; | ||||
| import { AddonModAssignOffline } from '../../services/assign-offline'; | ||||
| import { | ||||
|     AddonModAssignSyncProvider, | ||||
|     AddonModAssignSync, | ||||
|     AddonModAssignManualSyncData, | ||||
|     AddonModAssignAutoSyncData, | ||||
| } from '../../services/assign-sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays a list of submissions of an assignment. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-assign-submission-list', | ||||
|     templateUrl: 'submission-list.html', | ||||
| }) | ||||
| export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
| 
 | ||||
|     title = ''; // Title to display.
 | ||||
|     assign?: AddonModAssignAssign; // Assignment.
 | ||||
|     submissions: AddonModAssignSubmissionListManager; // List of submissions
 | ||||
|     loaded = false; // Whether data has been loaded.
 | ||||
|     haveAllParticipants  = true; // Whether all participants have been loaded.
 | ||||
|     groupId = 0; // Group ID to show.
 | ||||
|     courseId!: number; // Course ID the assignment belongs to.
 | ||||
|     moduleId!: number; // Module ID the submission belongs to.
 | ||||
| 
 | ||||
|     groupInfo: CoreGroupInfo = { | ||||
|         groups: [], | ||||
|         separateGroups: false, | ||||
|         visibleGroups: false, | ||||
|         defaultGroupId: 0, | ||||
|     }; | ||||
| 
 | ||||
|     protected selectedStatus?: string; // The status to see.
 | ||||
|     protected gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes.
 | ||||
|     protected syncObserver: CoreEventObserver; // Observer to refresh data when the async is synchronized.
 | ||||
|     protected submissionsData: { canviewsubmissions: boolean; submissions?: AddonModAssignSubmission[] } = { | ||||
|         canviewsubmissions: false, | ||||
|     }; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected route: ActivatedRoute, | ||||
|     ) { | ||||
|         this.submissions = new AddonModAssignSubmissionListManager(AddonModAssignSubmissionListPage); | ||||
| 
 | ||||
|         // Update data if some grade changes.
 | ||||
|         this.gradedObserver = CoreEvents.on<AddonModAssignGradedEventData>( | ||||
|             AddonModAssignProvider.GRADED_EVENT, | ||||
|             (data) => { | ||||
|                 if ( | ||||
|                     this.loaded && | ||||
|                     this.assign && | ||||
|                     data.assignmentId == this.assign.id && | ||||
|                     data.userId == CoreSites.instance.getCurrentSiteUserId() | ||||
|                 ) { | ||||
|                     // Grade changed, refresh the data.
 | ||||
|                     this.loaded = false; | ||||
| 
 | ||||
|                     this.refreshAllData(true).finally(() => { | ||||
|                         this.loaded = true; | ||||
|                     }); | ||||
|                 } | ||||
|             }, | ||||
|             CoreSites.instance.getCurrentSiteId(), | ||||
|         ); | ||||
| 
 | ||||
|         // Refresh data if this assign is synchronized.
 | ||||
|         const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED]; | ||||
|         this.syncObserver = CoreEvents.onMultiple<AddonModAssignAutoSyncData | AddonModAssignManualSyncData>( | ||||
|             events, | ||||
|             (data) => { | ||||
|                 if (!this.loaded || ('context' in data && data.context == 'submission-list')) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 this.loaded = false; | ||||
| 
 | ||||
|                 this.refreshAllData(false).finally(() => { | ||||
|                     this.loaded = true; | ||||
|                 }); | ||||
|             }, | ||||
|             CoreSites.instance.getCurrentSiteId(), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngAfterViewInit(): void { | ||||
|         this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!; | ||||
|         this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!; | ||||
|         this.groupId = CoreNavigator.instance.getRouteNumberParam('groupId') || 0; | ||||
|         this.selectedStatus = CoreNavigator.instance.getRouteParam('status'); | ||||
| 
 | ||||
|         if (this.selectedStatus) { | ||||
|             if (this.selectedStatus == AddonModAssignProvider.NEED_GRADING) { | ||||
|                 this.title = Translate.instance.instant('addon.mod_assign.numberofsubmissionsneedgrading'); | ||||
|             } else { | ||||
|                 this.title = Translate.instance.instant('addon.mod_assign.submissionstatus_' + this.selectedStatus); | ||||
|             } | ||||
|         } else { | ||||
|             this.title = Translate.instance.instant('addon.mod_assign.numberofparticipants'); | ||||
|         } | ||||
|         this.fetchAssignment(true).finally(() => { | ||||
|             this.loaded = true; | ||||
|             this.submissions.start(this.splitView); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch assignment data. | ||||
|      * | ||||
|      * @param sync Whether to try to synchronize data. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchAssignment(sync = false): Promise<void> { | ||||
|         try { | ||||
|             // Get assignment data.
 | ||||
|             this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); | ||||
| 
 | ||||
|             this.title = this.assign.name || this.title; | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 try { | ||||
|                     // Try to synchronize data.
 | ||||
|                     const result = await AddonModAssignSync.instance.syncAssign(this.assign.id); | ||||
| 
 | ||||
|                     if (result && result.updated) { | ||||
|                         CoreEvents.trigger<AddonModAssignManualSyncData>( | ||||
|                             AddonModAssignSyncProvider.MANUAL_SYNCED, | ||||
|                             { | ||||
|                                 assignId: this.assign.id, | ||||
|                                 warnings: result.warnings, | ||||
|                                 gradesBlocked: result.gradesBlocked, | ||||
|                                 context: 'submission-list', | ||||
|                             }, | ||||
|                             CoreSites.instance.getCurrentSiteId(), | ||||
|                         ); | ||||
|                     } | ||||
|                 } catch (error) { | ||||
|                     // Ignore errors, probably user is offline or sync is blocked.
 | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Get assignment submissions.
 | ||||
|             this.submissionsData = await AddonModAssign.instance.getSubmissions(this.assign.id, { cmId: this.assign.cmid }); | ||||
| 
 | ||||
|             if (!this.submissionsData.canviewsubmissions) { | ||||
|                 // User shouldn't be able to reach here.
 | ||||
|                 throw new Error('Cannot view submissions.'); | ||||
|             } | ||||
| 
 | ||||
|             // Check if groupmode is enabled to avoid showing wrong numbers.
 | ||||
|             this.groupInfo = await CoreGroups.instance.getActivityGroupInfo(this.assign.cmid, false); | ||||
| 
 | ||||
|             await this.setGroup(CoreGroups.instance.validateGroupId(this.groupId, this.groupInfo)); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting assigment data.'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set group to see the summary. | ||||
|      * | ||||
|      * @param groupId Group ID. | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     async setGroup(groupId: number): Promise<void> { | ||||
|         this.groupId = groupId; | ||||
| 
 | ||||
|         this.haveAllParticipants = true; | ||||
| 
 | ||||
|         if (!CoreSites.instance.getCurrentSite()?.wsAvailable('mod_assign_list_participants')) { | ||||
|             // Submissions are not displayed in Moodle 3.1 without the local plugin, see MOBILE-2968.
 | ||||
|             this.haveAllParticipants = false; | ||||
|             this.submissions.resetItems(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Fetch submissions and grades.
 | ||||
|         const submissions = | ||||
|             await AddonModAssignHelper.instance.getSubmissionsUserData( | ||||
|                 this.assign!, | ||||
|                 this.submissionsData.submissions, | ||||
|                 this.groupId, | ||||
|             ); | ||||
|         // Get assignment grades only if workflow is not enabled to check grading date.
 | ||||
|         const grades = !this.assign!.markingworkflow | ||||
|             ? await AddonModAssign.instance.getAssignmentGrades(this.assign!.id, { cmId: this.assign!.cmid }) | ||||
|             : []; | ||||
| 
 | ||||
|         // Filter the submissions to get only the ones with the right status and add some extra data.
 | ||||
|         const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING; | ||||
|         const searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus; | ||||
| 
 | ||||
|         const promises: Promise<void>[] = []; | ||||
|         const showSubmissions: AddonModAssignSubmissionForList[] = []; | ||||
| 
 | ||||
|         submissions.forEach((submission: AddonModAssignSubmissionForList) => { | ||||
|             if (!searchStatus || searchStatus == submission.status) { | ||||
|                 promises.push( | ||||
|                     CoreUtils.instance.ignoreErrors( | ||||
|                         AddonModAssignOffline.instance.getSubmissionGrade(this.assign!.id, submission.userid), | ||||
|                     ).then(async (data) => { | ||||
|                         if (getNeedGrading) { | ||||
|                             // Only show the submissions that need to be graded.
 | ||||
|                             const add = await AddonModAssign.instance.needsSubmissionToBeGraded(submission, this.assign!.id); | ||||
| 
 | ||||
|                             if (!add) { | ||||
|                                 return; | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         // Load offline grades.
 | ||||
|                         const notSynced = !!data && submission.timemodified < data.timemodified; | ||||
| 
 | ||||
|                         if (submission.gradingstatus == 'graded' && !this.assign!.markingworkflow) { | ||||
|                             // Get the last grade of the submission.
 | ||||
|                             const grade = grades | ||||
|                                 .filter((grade) => grade.userid == submission.userid) | ||||
|                                 .reduce((a, b) => (a.timemodified > b.timemodified ? a : b)); | ||||
| 
 | ||||
|                             if (grade && grade.timemodified < submission.timemodified) { | ||||
|                                 submission.gradingstatus = AddonModAssignProvider.GRADED_FOLLOWUP_SUBMIT; | ||||
|                             } | ||||
|                         } | ||||
|                         submission.statusColor = AddonModAssign.instance.getSubmissionStatusColor(submission.status); | ||||
|                         submission.gradingColor = AddonModAssign.instance.getSubmissionGradingStatusColor( | ||||
|                             submission.gradingstatus, | ||||
|                         ); | ||||
| 
 | ||||
|                         // Show submission status if not submitted for grading.
 | ||||
|                         if (submission.statusColor != 'success' || !submission.gradingstatus) { | ||||
|                             submission.statusTranslated = Translate.instance.instant( | ||||
|                                 'addon.mod_assign.submissionstatus_' + submission.status, | ||||
|                             ); | ||||
|                         } else { | ||||
|                             submission.statusTranslated = ''; | ||||
|                         } | ||||
| 
 | ||||
|                         if (notSynced) { | ||||
|                             submission.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced'; | ||||
|                             submission.gradingColor = ''; | ||||
|                         } else if (submission.statusColor != 'danger' || submission.gradingColor != 'danger') { | ||||
|                             // Show grading status if one of the statuses is not done.
 | ||||
|                             submission.gradingStatusTranslationId = AddonModAssign.instance.getSubmissionGradingStatusTranslationId( | ||||
|                                 submission.gradingstatus, | ||||
|                             ); | ||||
|                         } else { | ||||
|                             submission.gradingStatusTranslationId = ''; | ||||
|                         } | ||||
| 
 | ||||
|                         showSubmissions.push(submission); | ||||
| 
 | ||||
|                         return; | ||||
|                     }), | ||||
|                 ); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         this.submissions.setItems(showSubmissions); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh all the data. | ||||
|      * | ||||
|      * @param sync Whether to try to synchronize data. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async refreshAllData(sync?: boolean): Promise<void> { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId)); | ||||
|         if (this.assign) { | ||||
|             promises.push(AddonModAssign.instance.invalidateAllSubmissionData(this.assign.id)); | ||||
|             promises.push(AddonModAssign.instance.invalidateAssignmentUserMappingsData(this.assign.id)); | ||||
|             promises.push(AddonModAssign.instance.invalidateAssignmentGradesData(this.assign.id)); | ||||
|             promises.push(AddonModAssign.instance.invalidateListParticipantsData(this.assign.id)); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await Promise.all(promises); | ||||
|         } finally { | ||||
|             this.fetchAssignment(sync); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the list. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      */ | ||||
|     refreshList(refresher?: CustomEvent<IonRefresher>): void { | ||||
|         this.refreshAllData(true).finally(() => { | ||||
|             refresher?.detail.complete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.gradedObserver?.off(); | ||||
|         this.syncObserver?.off(); | ||||
|         this.submissions.destroy(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Helper class to manage submissions. | ||||
|  */ | ||||
| class AddonModAssignSubmissionListManager extends CorePageItemsListManager<AddonModAssignSubmissionForList> { | ||||
| 
 | ||||
|     constructor(pageComponent: unknown) { | ||||
|         super(pageComponent); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected getItemPath(submission: AddonModAssignSubmissionForList): string { | ||||
|         return String(submission.submitid); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected getItemQueryParams(submission: AddonModAssignSubmissionForList): Params { | ||||
|         return CoreObject.withoutEmpty({ | ||||
|             blindId: submission.blindid, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null { | ||||
|         return route.params.submitId ?? null; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Calculated data for an assign submission. | ||||
|  */ | ||||
| type AddonModAssignSubmissionForList = AddonModAssignSubmissionFormatted & { | ||||
|     statusColor?: string; // Calculated in the app. Color of the submission status.
 | ||||
|     gradingColor?: string; // Calculated in the app. Color of the submission grading status.
 | ||||
|     statusTranslated?: string; // Calculated in the app. Translated text of the submission status.
 | ||||
|     gradingStatusTranslationId?: string; // Calculated in the app. Key of the text of the submission grading status.
 | ||||
| }; | ||||
| @ -0,0 +1,29 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|             <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </ion-title> | ||||
| 
 | ||||
|         <ion-buttons slot="end"></ion-buttons> | ||||
|     </ion-toolbar> | ||||
| 
 | ||||
|     <core-navbar-buttons slot="end"> | ||||
|         <ion-button [hidden]="!canSaveGrades" fill="clear" (click)="submitGrade()" [attr.aria-label]="'core.done' | translate"> | ||||
|             {{ 'core.done' | translate }} | ||||
|         </ion-button> | ||||
|     </core-navbar-buttons> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
| 
 | ||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <addon-mod-assign-submission [courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId"> | ||||
|         </addon-mod-assign-submission> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
| @ -0,0 +1,184 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, ViewChild } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { AddonModAssignSubmissionComponent } from '../../components/submission/submission'; | ||||
| import { AddonModAssign, AddonModAssignAssign } from '../../services/assign'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays a submission. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-assign-submission-review', | ||||
|     templateUrl: 'submission-review.html', | ||||
| }) | ||||
| export class AddonModAssignSubmissionReviewPage implements OnInit { | ||||
| 
 | ||||
|     @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent; | ||||
| 
 | ||||
|     title = ''; // Title to display.
 | ||||
|     moduleId!: number; // Module ID the submission belongs to.
 | ||||
|     courseId!: number; // Course ID the assignment belongs to.
 | ||||
|     submitId!: number; //  User that did the submission.
 | ||||
|     blindId?: number; // Blinded user ID (if it's blinded).
 | ||||
|     loaded = false; // Whether data has been loaded.
 | ||||
|     canSaveGrades = false; // Whether the user can save grades.
 | ||||
| 
 | ||||
|     protected assign?: AddonModAssignAssign; // The assignment the submission belongs to.
 | ||||
|     protected blindMarking = false; // Whether it uses blind marking.
 | ||||
|     protected forceLeave = false; // To allow leaving the page without checking for changes.
 | ||||
| 
 | ||||
| 
 | ||||
|     constructor( | ||||
|         protected route: ActivatedRoute, | ||||
|     ) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.route.queryParams.subscribe((params) => { | ||||
|             this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!; | ||||
|             this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!; | ||||
|             this.submitId = CoreNavigator.instance.getRouteNumberParam('submitId') || 0; | ||||
|             this.blindId = CoreNavigator.instance.getRouteNumberParam('blindId', params); | ||||
| 
 | ||||
|             this.fetchSubmission().finally(() => { | ||||
|                 this.loaded = true; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we can leave the page or not. | ||||
|      * | ||||
|      * @return Resolved if we can leave it, rejected if not. | ||||
|      */ | ||||
|     ionViewCanLeave(): boolean | Promise<void> { | ||||
|         if (!this.submissionComponent || this.forceLeave) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // Check if data has changed.
 | ||||
|         return this.submissionComponent.canLeave(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User entered the page. | ||||
|      */ | ||||
|     ionViewDidEnter(): void { | ||||
|         this.submissionComponent?.ionViewDidEnter(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User left the page. | ||||
|      */ | ||||
|     ionViewDidLeave(): void { | ||||
|         this.submissionComponent?.ionViewDidLeave(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the submission. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchSubmission(): Promise<void> { | ||||
|         this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); | ||||
|         this.title = this.assign.name; | ||||
| 
 | ||||
|         this.blindMarking = !!this.assign.blindmarking && !this.assign.revealidentities; | ||||
| 
 | ||||
|         const gradeInfo = await CoreCourse.instance.getModuleBasicGradeInfo(this.moduleId); | ||||
|         if (!gradeInfo) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Grades can be saved if simple grading.
 | ||||
|         if (gradeInfo.advancedgrading && gradeInfo.advancedgrading[0] && | ||||
|                 typeof gradeInfo.advancedgrading[0].method != 'undefined') { | ||||
| 
 | ||||
|             const method = gradeInfo.advancedgrading[0].method || 'simple'; | ||||
|             this.canSaveGrades = method == 'simple'; | ||||
|         } else { | ||||
|             this.canSaveGrades = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh all the data. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async refreshAllData(): Promise<void> { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId)); | ||||
|         if (this.assign) { | ||||
|             promises.push(AddonModAssign.instance.invalidateSubmissionData(this.assign.id)); | ||||
|             promises.push(AddonModAssign.instance.invalidateAssignmentUserMappingsData(this.assign.id)); | ||||
|             promises.push(AddonModAssign.instance.invalidateSubmissionStatusData( | ||||
|                 this.assign.id, | ||||
|                 this.submitId, | ||||
|                 undefined, | ||||
|                 this.blindMarking, | ||||
|             )); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await Promise.all(promises); | ||||
|         } finally { | ||||
|             this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true); | ||||
| 
 | ||||
|             await this.fetchSubmission(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the data. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      */ | ||||
|     refreshSubmission(refresher?: CustomEvent<IonRefresher>): void { | ||||
|         this.refreshAllData().finally(() => { | ||||
|             refresher?.detail.complete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Submit a grade and feedback. | ||||
|      */ | ||||
|     async submitGrade(): Promise<void> { | ||||
|         if (!this.submissionComponent) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await this.submissionComponent.submitGrade(); | ||||
|             // Grade submitted, leave the view if not in tablet.
 | ||||
|             if (!CoreScreen.instance.isTablet) { | ||||
|                 this.forceLeave = true; | ||||
|                 CoreNavigator.instance.back(); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										731
									
								
								src/addons/mod/assign/services/assign-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										731
									
								
								src/addons/mod/assign/services/assign-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,731 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; | ||||
| import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { FileEntry } from '@ionic-native/file/ngx'; | ||||
| import { | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssignParticipant, | ||||
|     AddonModAssignSubmissionFeedback, | ||||
|     AddonModAssign, | ||||
|     AddonModAssignPlugin, | ||||
|     AddonModAssignSavePluginData, | ||||
| } from './assign'; | ||||
| import { AddonModAssignOffline } from './assign-offline'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; | ||||
| import { CoreGroups } from '@services/groups'; | ||||
| import { AddonModAssignSubmissionDelegate } from './submission-delegate'; | ||||
| import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some helper functions for assign. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignHelperProvider { | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a submission can be edited in offline. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param submission Submission. | ||||
|      * @return Whether it can be edited offline. | ||||
|      */ | ||||
|     async canEditSubmissionOffline(assign: AddonModAssignAssign, submission?: AddonModAssignSubmission): Promise<boolean> { | ||||
|         if (!submission) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         if (submission.status == AddonModAssignProvider.SUBMISSION_STATUS_NEW || | ||||
|                 submission.status == AddonModAssignProvider.SUBMISSION_STATUS_REOPENED) { | ||||
|             // It's a new submission, allow creating it in offline.
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         let canEdit = true; | ||||
| 
 | ||||
|         const promises = submission.plugins | ||||
|             ? submission.plugins.map((plugin) => | ||||
|                 AddonModAssignSubmissionDelegate.instance.canPluginEditOffline(assign, submission, plugin).then((canEditPlugin) => { | ||||
|                     if (!canEditPlugin) { | ||||
|                         canEdit = false; | ||||
|                     } | ||||
| 
 | ||||
|                     return; | ||||
|                 })) | ||||
|             : []; | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         return canEdit; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Clear plugins temporary data because a submission was cancelled. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param submission Submission to clear the data for. | ||||
|      * @param inputData Data entered in the submission form. | ||||
|      */ | ||||
|     clearSubmissionPluginTmpData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission | undefined, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): void { | ||||
|         if (!submission) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         submission.plugins?.forEach((plugin) => { | ||||
|             AddonModAssignSubmissionDelegate.instance.clearTmpData(assign, submission, plugin, inputData); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Copy the data from last submitted attempt to the current submission. | ||||
|      * Since we don't have any WS for that we'll have to re-submit everything manually. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param previousSubmission Submission to copy. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async copyPreviousAttempt(assign: AddonModAssignAssign, previousSubmission: AddonModAssignSubmission): Promise<void> { | ||||
|         const pluginData: AddonModAssignSavePluginData = {}; | ||||
|         const promises = previousSubmission.plugins | ||||
|             ? previousSubmission.plugins.map((plugin) => | ||||
|                 AddonModAssignSubmissionDelegate.instance.copyPluginSubmissionData(assign, plugin, pluginData)) | ||||
|             : []; | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         // We got the plugin data. Now we need to submit it.
 | ||||
|         if (Object.keys(pluginData).length) { | ||||
|             // There's something to save.
 | ||||
|             return AddonModAssign.instance.saveSubmissionOnline(assign.id, pluginData); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create an empty feedback object. | ||||
|      * | ||||
|      * @return Feedback. | ||||
|      */ | ||||
|     createEmptyFeedback(): AddonModAssignSubmissionFeedback { | ||||
|         return { | ||||
|             grade: undefined, | ||||
|             gradefordisplay: '', | ||||
|             gradeddate: 0, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create an empty submission object. | ||||
|      * | ||||
|      * @return Submission. | ||||
|      */ | ||||
|     createEmptySubmission(): AddonModAssignSubmissionFormatted { | ||||
|         return { | ||||
|             id: 0, | ||||
|             userid: 0, | ||||
|             attemptnumber: 0, | ||||
|             timecreated: 0, | ||||
|             timemodified: 0, | ||||
|             status: '', | ||||
|             groupid: 0, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete stored submission files for a plugin. See storeSubmissionFiles. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins). | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteStoredSubmissionFiles(assignId: number, folderName: string, userId?: number, siteId?: string): Promise<void> { | ||||
|         const folderPath = await AddonModAssignOffline.instance.getSubmissionPluginFolder(assignId, folderName, userId, siteId); | ||||
| 
 | ||||
|         await CoreFile.instance.removeDir(folderPath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all drafts of the feedback plugin data. | ||||
|      * | ||||
|      * @param assignId Assignment Id. | ||||
|      * @param userId User Id. | ||||
|      * @param feedback Feedback data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async discardFeedbackPluginData( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         feedback: AddonModAssignSubmissionFeedback, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const promises = feedback.plugins | ||||
|             ? feedback.plugins.map((plugin) => | ||||
|                 AddonModAssignFeedbackDelegate.instance.discardPluginFeedbackData(assignId, userId, plugin, siteId)) | ||||
|             : []; | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a submission has no content. | ||||
|      * | ||||
|      * @param assign Assignment object. | ||||
|      * @param submission Submission to inspect. | ||||
|      * @return Whether the submission is empty. | ||||
|      */ | ||||
|     isSubmissionEmpty(assign: AddonModAssignAssign, submission?: AddonModAssignSubmission): boolean { | ||||
|         if (!submission) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         const anyNotEmpty = submission.plugins?.some((plugin) => | ||||
|             !AddonModAssignSubmissionDelegate.instance.isPluginEmpty(assign, plugin)); | ||||
| 
 | ||||
|         // If any plugin is not empty, we consider that the submission is not empty either.
 | ||||
|         if (anyNotEmpty) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         // If all the plugins were empty (or there were no plugins), we consider the submission to be empty.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * List the participants for a single assignment, with some summary info about their submissions. | ||||
|      * | ||||
|      * @param assign Assignment object. | ||||
|      * @param groupId Group Id. | ||||
|      * @param options Other options. | ||||
|      * @return Promise resolved with the list of participants and summary of submissions. | ||||
|      */ | ||||
|     async getParticipants( | ||||
|         assign: AddonModAssignAssign, | ||||
|         groupId?: number, | ||||
|         options: CoreSitesCommonWSOptions = {}, | ||||
|     ): Promise<AddonModAssignParticipant[]> { | ||||
| 
 | ||||
|         groupId = groupId || 0; | ||||
|         options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Create new options including all existing ones.
 | ||||
|         const modOptions: CoreCourseCommonModWSOptions = { cmId: assign.cmid, ...options }; | ||||
| 
 | ||||
|         const participants = await AddonModAssign.instance.listParticipants(assign.id, groupId, modOptions); | ||||
| 
 | ||||
|         if (groupId || participants && participants.length > 0) { | ||||
|             return participants; | ||||
|         } | ||||
| 
 | ||||
|         // If no participants returned and all groups specified, get participants by groups.
 | ||||
|         const groupsInfo = await CoreGroups.instance.getActivityGroupInfo(assign.cmid, false, undefined, modOptions.siteId); | ||||
| 
 | ||||
|         const participantsIndexed: {[id: number]: AddonModAssignParticipant} = {}; | ||||
| 
 | ||||
|         const promises = groupsInfo.groups | ||||
|             ? groupsInfo.groups.map((userGroup) => | ||||
|                 AddonModAssign.instance.listParticipants(assign.id, userGroup.id, modOptions).then((participantsFromList) => { | ||||
|                     // Do not get repeated users.
 | ||||
|                     participantsFromList.forEach((participant) => { | ||||
|                         participantsIndexed[participant.id] = participant; | ||||
|                     }); | ||||
| 
 | ||||
|                     return; | ||||
|                 })) | ||||
|             :[]; | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         return CoreUtils.instance.objectToArray(participantsIndexed); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get plugin config from assignment config. | ||||
|      * | ||||
|      * @param assign Assignment object including all config. | ||||
|      * @param subtype Subtype name (assignsubmission or assignfeedback) | ||||
|      * @param type Name of the subplugin. | ||||
|      * @return Object containing all configurations of the subplugin selected. | ||||
|      */ | ||||
|     getPluginConfig(assign: AddonModAssignAssign, subtype: string, type: string): AddonModAssignPluginConfig { | ||||
|         const configs: AddonModAssignPluginConfig = {}; | ||||
| 
 | ||||
|         assign.configs.forEach((config) => { | ||||
|             if (config.subtype == subtype && config.plugin == type) { | ||||
|                 configs[config.name] = config.value; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         return configs; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get enabled subplugins. | ||||
|      * | ||||
|      * @param assign Assignment object including all config. | ||||
|      * @param subtype Subtype name (assignsubmission or assignfeedback) | ||||
|      * @return List of enabled plugins for the assign. | ||||
|      */ | ||||
|     getPluginsEnabled(assign: AddonModAssignAssign, subtype: string): AddonModAssignPlugin[] { | ||||
|         const enabled: AddonModAssignPlugin[] = []; | ||||
| 
 | ||||
|         assign.configs.forEach((config) => { | ||||
|             if (config.subtype == subtype && config.name == 'enabled' && parseInt(config.value, 10) === 1) { | ||||
|                 // Format the plugin objects.
 | ||||
|                 enabled.push({ | ||||
|                     type: config.plugin, | ||||
|                     name: config.plugin, | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         return enabled; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a list of stored submission files. See storeSubmissionFiles. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins). | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     async getStoredSubmissionFiles( | ||||
|         assignId: number, | ||||
|         folderName: string, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<(FileEntry | DirectoryEntry)[]> { | ||||
|         const folderPath = await AddonModAssignOffline.instance.getSubmissionPluginFolder(assignId, folderName, userId, siteId); | ||||
| 
 | ||||
|         return CoreFile.instance.getDirectoryContents(folderPath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size that will be uploaded to perform an attempt copy. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param previousSubmission Submission to copy. | ||||
|      * @return Promise resolved with the size. | ||||
|      */ | ||||
|     async getSubmissionSizeForCopy(assign: AddonModAssignAssign, previousSubmission: AddonModAssignSubmission): Promise<number> { | ||||
|         let totalSize = 0; | ||||
| 
 | ||||
|         const promises = previousSubmission.plugins | ||||
|             ? previousSubmission.plugins.map((plugin) => | ||||
|                 AddonModAssignSubmissionDelegate.instance.getPluginSizeForCopy(assign, plugin).then((size) => { | ||||
|                     totalSize += (size || 0); | ||||
| 
 | ||||
|                     return; | ||||
|                 })) | ||||
|             : []; | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         return totalSize; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size that will be uploaded to save a submission. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param submission Submission to check data. | ||||
|      * @param inputData Data entered in the submission form. | ||||
|      * @return Promise resolved with the size. | ||||
|      */ | ||||
|     async getSubmissionSizeForEdit( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): Promise<number> { | ||||
| 
 | ||||
|         let totalSize = 0; | ||||
| 
 | ||||
|         const promises = submission.plugins | ||||
|             ? submission.plugins.map((plugin) => | ||||
|                 AddonModAssignSubmissionDelegate.instance.getPluginSizeForEdit(assign, submission, plugin, inputData) | ||||
|                     .then((size) => { | ||||
|                         totalSize += (size || 0); | ||||
| 
 | ||||
|                         return; | ||||
|                     })) | ||||
|             : []; | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         return totalSize; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get user data for submissions since they only have userid. | ||||
|      * | ||||
|      * @param assign Assignment object. | ||||
|      * @param submissions Submissions to get the data for. | ||||
|      * @param groupId Group Id. | ||||
|      * @param options Other options. | ||||
|      * @return Promise always resolved. Resolve param is the formatted submissions. | ||||
|      */ | ||||
|     async getSubmissionsUserData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submissions: AddonModAssignSubmissionFormatted[] = [], | ||||
|         groupId?: number, | ||||
|         options: CoreSitesCommonWSOptions = {}, | ||||
|     ): Promise<AddonModAssignSubmissionFormatted[]> { | ||||
|         // Create new options including all existing ones.
 | ||||
|         const modOptions: CoreCourseCommonModWSOptions = { cmId: assign.cmid, ...options }; | ||||
| 
 | ||||
|         const parts = await this.getParticipants(assign, groupId, options); | ||||
| 
 | ||||
|         const blind = assign.blindmarking && !assign.revealidentities; | ||||
|         const promises: Promise<void>[] = []; | ||||
|         const result: AddonModAssignSubmissionFormatted[] = []; | ||||
|         const participants: {[id: number]: AddonModAssignParticipant} = CoreUtils.instance.arrayToObject(parts, 'id'); | ||||
| 
 | ||||
|         submissions.forEach((submission) => { | ||||
|             submission.submitid = submission.userid && submission.userid > 0 ? submission.userid : submission.blindid; | ||||
|             if (typeof submission.submitid == 'undefined' || submission.submitid <= 0) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const participant = participants[submission.submitid]; | ||||
|             if (!participant) { | ||||
|                 // Avoid permission denied error. Participant not found on list.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             delete participants[submission.submitid]; | ||||
| 
 | ||||
|             if (!blind) { | ||||
|                 submission.userfullname = participant.fullname; | ||||
|                 submission.userprofileimageurl = participant.profileimageurl; | ||||
|             } | ||||
| 
 | ||||
|             submission.manyGroups = !!participant.groups && participant.groups.length > 1; | ||||
|             submission.noGroups = !!participant.groups && participant.groups.length == 0; | ||||
|             if (participant.groupname) { | ||||
|                 submission.groupid = participant.groupid!; | ||||
|                 submission.groupname = participant.groupname; | ||||
|             } | ||||
| 
 | ||||
|             let promise = Promise.resolve(); | ||||
|             if (submission.userid && submission.userid > 0 && blind) { | ||||
|                 // Blind but not blinded! (Moodle < 3.1.1, 3.2).
 | ||||
|                 delete submission.userid; | ||||
| 
 | ||||
|                 promise = AddonModAssign.instance.getAssignmentUserMappings(assign.id, submission.submitid, modOptions) | ||||
|                     .then((blindId) => { | ||||
|                         submission.blindid = blindId; | ||||
| 
 | ||||
|                         return; | ||||
|                     }); | ||||
|             } | ||||
| 
 | ||||
|             promises.push(promise.then(() => { | ||||
|                 // Add to the list.
 | ||||
|                 if (submission.userfullname || submission.blindid) { | ||||
|                     result.push(submission); | ||||
|                 } | ||||
| 
 | ||||
|                 return; | ||||
|             })); | ||||
|         }); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         // Create a submission for each participant left in the list (the participants already treated were removed).
 | ||||
|         CoreUtils.instance.objectToArray(participants).forEach((participant: AddonModAssignParticipant) => { | ||||
|             const submission = this.createEmptySubmission(); | ||||
| 
 | ||||
|             submission.submitid = participant.id; | ||||
| 
 | ||||
|             if (!blind) { | ||||
|                 submission.userid = participant.id; | ||||
|                 submission.userfullname = participant.fullname; | ||||
|                 submission.userprofileimageurl = participant.profileimageurl; | ||||
|             } else { | ||||
|                 submission.blindid = participant.id; | ||||
|             } | ||||
| 
 | ||||
|             submission.manyGroups = !!participant.groups && participant.groups.length > 1; | ||||
|             submission.noGroups = !!participant.groups && participant.groups.length == 0; | ||||
|             if (participant.groupname) { | ||||
|                 submission.groupid = participant.groupid!; | ||||
|                 submission.groupname = participant.groupname; | ||||
|             } | ||||
|             submission.status = participant.submitted ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : | ||||
|                 AddonModAssignProvider.SUBMISSION_STATUS_NEW; | ||||
| 
 | ||||
|             result.push(submission); | ||||
|         }); | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the feedback data has changed for a certain submission and assign. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param feedback Feedback data. | ||||
|      * @param userId The user ID. | ||||
|      * @return Promise resolved with true if data has changed, resolved with false otherwise. | ||||
|      */ | ||||
|     async hasFeedbackDataChanged( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted | undefined, | ||||
|         feedback: AddonModAssignSubmissionFeedback, | ||||
|         userId: number, | ||||
|     ): Promise<boolean> { | ||||
|         if (!submission || !feedback.plugins) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         let hasChanged = false; | ||||
| 
 | ||||
|         const promises = feedback.plugins.map((plugin) => | ||||
|             this.prepareFeedbackPluginData(assign.id, userId, feedback).then(async (inputData) => { | ||||
|                 const changed = await CoreUtils.instance.ignoreErrors( | ||||
|                     AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData, userId), | ||||
|                     false, | ||||
|                 ); | ||||
|                 if (changed) { | ||||
|                     hasChanged = true; | ||||
|                 } | ||||
| 
 | ||||
|                 return; | ||||
|             })); | ||||
| 
 | ||||
|         await CoreUtils.instance.allPromises(promises); | ||||
| 
 | ||||
|         return hasChanged; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the submission data has changed for a certain submission and assign. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param submission Submission to check data. | ||||
|      * @param inputData Data entered in the submission form. | ||||
|      * @return Promise resolved with true if data has changed, resolved with false otherwise. | ||||
|      */ | ||||
|     async hasSubmissionDataChanged( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission | undefined, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): Promise<boolean> { | ||||
|         if (!submission) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         let hasChanged = false; | ||||
| 
 | ||||
|         const promises = submission.plugins | ||||
|             ? submission.plugins.map((plugin) => | ||||
|                 AddonModAssignSubmissionDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData) | ||||
|                     .then((changed) => { | ||||
|                         if (changed) { | ||||
|                             hasChanged = true; | ||||
|                         } | ||||
| 
 | ||||
|                         return; | ||||
|                     }).catch(() => { | ||||
|                         // Ignore errors.
 | ||||
|                     })) | ||||
|             : []; | ||||
| 
 | ||||
|         await CoreUtils.instance.allPromises(promises); | ||||
| 
 | ||||
|         return hasChanged; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and return the plugin data to send for a certain feedback and assign. | ||||
|      * | ||||
|      * @param assignId Assignment Id. | ||||
|      * @param userId User Id. | ||||
|      * @param feedback Feedback data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with plugin data to send to server. | ||||
|      */ | ||||
|     async prepareFeedbackPluginData( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         feedback: AddonModAssignSubmissionFeedback, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModAssignSavePluginData> { | ||||
| 
 | ||||
|         const pluginData: Record<string, unknown> = {}; | ||||
|         const promises = feedback.plugins | ||||
|             ? feedback.plugins.map((plugin) => | ||||
|                 AddonModAssignFeedbackDelegate.instance.preparePluginFeedbackData(assignId, userId, plugin, pluginData, siteId)) | ||||
|             : []; | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         return pluginData; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and return the plugin data to send for a certain submission and assign. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param submission Submission to check data. | ||||
|      * @param inputData Data entered in the submission form. | ||||
|      * @param offline True to prepare the data for an offline submission, false otherwise. | ||||
|      * @return Promise resolved with plugin data to send to server. | ||||
|      */ | ||||
|     async prepareSubmissionPluginData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission | undefined, | ||||
|         inputData: Record<string, unknown>, | ||||
|         offline = false, | ||||
|     ): Promise<AddonModAssignSavePluginData> { | ||||
| 
 | ||||
|         if (!submission || !submission.plugins) { | ||||
|             return {}; | ||||
|         } | ||||
| 
 | ||||
|         const pluginData: AddonModAssignSavePluginData = {}; | ||||
|         const promises = submission.plugins.map((plugin) => | ||||
|             AddonModAssignSubmissionDelegate.instance.preparePluginSubmissionData( | ||||
|                 assign, | ||||
|                 submission, | ||||
|                 plugin, | ||||
|                 inputData, | ||||
|                 pluginData, | ||||
|                 offline, | ||||
|             )); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         return pluginData; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a list of files (either online files or local files), store the local files in a local folder | ||||
|      * to be submitted later. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins). | ||||
|      * @param files List of files. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if success, rejected otherwise. | ||||
|      */ | ||||
|     async storeSubmissionFiles( | ||||
|         assignId: number, | ||||
|         folderName: string, | ||||
|         files: (CoreWSExternalFile | FileEntry)[], | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreFileUploaderStoreFilesResult> { | ||||
|         // Get the folder where to store the files.
 | ||||
|         const folderPath = await AddonModAssignOffline.instance.getSubmissionPluginFolder(assignId, folderName, userId, siteId); | ||||
| 
 | ||||
|         return CoreFileUploader.instance.storeFilesToUpload(folderPath, files); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Upload a file to a draft area. If the file is an online file it will be downloaded and then re-uploaded. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param file Online file or local FileEntry. | ||||
|      * @param itemId Draft ID to use. Undefined or 0 to create a new draft ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the itemId. | ||||
|      */ | ||||
|     uploadFile(assignId: number, file: CoreWSExternalFile | FileEntry, itemId?: number, siteId?: string): Promise<number> { | ||||
|         return CoreFileUploader.instance.uploadOrReuploadFile(file, itemId, AddonModAssignProvider.COMPONENT, assignId, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a list of files (either online files or local files), upload them to a draft area and return the draft ID. | ||||
|      * Online files will be downloaded and then re-uploaded. | ||||
|      * If there are no files to upload it will return a fake draft ID (1). | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param files List of files. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the itemId. | ||||
|      */ | ||||
|     uploadFiles(assignId: number, files: (CoreWSExternalFile | FileEntry)[], siteId?: string): Promise<number> { | ||||
|         return CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModAssignProvider.COMPONENT, assignId, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Upload or store some files, depending if the user is offline or not. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins). | ||||
|      * @param files List of files. | ||||
|      * @param offline True if files sould be stored for offline, false to upload them. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async uploadOrStoreFiles( | ||||
|         assignId: number, | ||||
|         folderName: string, | ||||
|         files: (CoreWSExternalFile | FileEntry)[], | ||||
|         offline = false, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<number | CoreFileUploaderStoreFilesResult> { | ||||
| 
 | ||||
|         if (offline) { | ||||
|             return await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         return await this.uploadFiles(assignId, files, siteId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignHelper = makeSingleton(AddonModAssignHelperProvider); | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Assign submission with some calculated data. | ||||
|  */ | ||||
| export type AddonModAssignSubmissionFormatted = | ||||
|     Omit<AddonModAssignSubmission, 'userid'> & { | ||||
|         userid?: number; // Student id.
 | ||||
|         blindid?: number; // Calculated in the app. Blindid of the user that did the submission.
 | ||||
|         submitid?: number; // Calculated in the app. Userid or blindid of the user that did the submission.
 | ||||
|         userfullname?: string; // Calculated in the app. Full name of the user that did the submission.
 | ||||
|         userprofileimageurl?: string; // Calculated in the app. Avatar of the user that did the submission.
 | ||||
|         manyGroups?: boolean; // Calculated in the app. Whether the user belongs to more than 1 group.
 | ||||
|         noGroups?: boolean; // Calculated in the app. Whether the user doesn't belong to any group.
 | ||||
|         groupname?: string; // Calculated in the app. Name of the group the submission belongs to.
 | ||||
|     }; | ||||
| 
 | ||||
| /** | ||||
|  * Assignment plugin config. | ||||
|  */ | ||||
| export type AddonModAssignPluginConfig = {[name: string]: string}; | ||||
							
								
								
									
										459
									
								
								src/addons/mod/assign/services/assign-offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										459
									
								
								src/addons/mod/assign/services/assign-offline.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,459 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { SQLiteDBRecordValues } from '@classes/sqlitedb'; | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreTimeUtils } from '@services/utils/time'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonModAssignOutcomes, AddonModAssignSavePluginData } from './assign'; | ||||
| import { | ||||
|     AddonModAssignSubmissionsDBRecord, | ||||
|     AddonModAssignSubmissionsGradingDBRecord, | ||||
|     SUBMISSIONS_GRADES_TABLE, | ||||
|     SUBMISSIONS_TABLE, | ||||
| } from './database/assign'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle offline assign. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignOfflineProvider { | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a submission. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if deleted, rejected if failure. | ||||
|      */ | ||||
|     async deleteSubmission(assignId: number, userId?: number, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         userId = userId || site.getUserId(); | ||||
| 
 | ||||
|         await site.getDb().deleteRecords( | ||||
|             SUBMISSIONS_TABLE, | ||||
|             { assignid: assignId, userid: userId }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a submission grade. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if deleted, rejected if failure. | ||||
|      */ | ||||
|     async deleteSubmissionGrade(assignId: number, userId?: number, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         userId = userId || site.getUserId(); | ||||
| 
 | ||||
|         await site.getDb().deleteRecords( | ||||
|             SUBMISSIONS_GRADES_TABLE, | ||||
|             { assignid: assignId, userid: userId }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all the assignments ids that have something to be synced. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with assignments id that have something to be synced. | ||||
|      */ | ||||
|     async getAllAssigns(siteId?: string): Promise<number[]> { | ||||
|         const promises: | ||||
|         Promise<AddonModAssignSubmissionsDBRecordFormatted[] | AddonModAssignSubmissionsGradingDBRecordFormatted[]>[] = []; | ||||
| 
 | ||||
|         promises.push(this.getAllSubmissions(siteId)); | ||||
|         promises.push(this.getAllSubmissionsGrade(siteId)); | ||||
| 
 | ||||
|         const results = await Promise.all(promises); | ||||
|         // Flatten array.
 | ||||
|         const flatten: (AddonModAssignSubmissionsDBRecord | AddonModAssignSubmissionsGradingDBRecord)[] = | ||||
|             [].concat.apply([], results); | ||||
| 
 | ||||
|         // Get assign id.
 | ||||
|         let assignIds: number[] = flatten.map((assign) => assign.assignid); | ||||
|         // Get unique values.
 | ||||
|         assignIds = assignIds.filter((id, pos) => assignIds.indexOf(id) == pos); | ||||
| 
 | ||||
|         return assignIds; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all the stored submissions from all the assignments. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with submissions. | ||||
|      */ | ||||
|     protected async getAllSubmissions(siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> { | ||||
|         return this.getAssignSubmissionsFormatted(undefined, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all the stored submissions for a certain assignment. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with submissions. | ||||
|      */ | ||||
|     async getAssignSubmissions(assignId: number, siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> { | ||||
|         return this.getAssignSubmissionsFormatted({ assignid: assignId }, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience helper function to get stored submissions formatted. | ||||
|      * | ||||
|      * @param conditions Query conditions. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with submissions. | ||||
|      */ | ||||
|     protected async getAssignSubmissionsFormatted( | ||||
|         conditions: SQLiteDBRecordValues = {}, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const submissions: AddonModAssignSubmissionsDBRecord[] = await db.getRecords(SUBMISSIONS_TABLE, conditions); | ||||
| 
 | ||||
|         // Parse the plugin data.
 | ||||
|         return submissions.map((submission) => ({ | ||||
|             assignid: submission.assignid, | ||||
|             userid: submission.userid, | ||||
|             courseid: submission.courseid, | ||||
|             plugindata: CoreTextUtils.instance.parseJSON<AddonModAssignSavePluginData>(submission.plugindata, {}), | ||||
|             onlinetimemodified: submission.onlinetimemodified, | ||||
|             timecreated: submission.timecreated, | ||||
|             timemodified: submission.timemodified, | ||||
|             submitted: submission.submitted, | ||||
|             submissionstatement: submission.submissionstatement, | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all the stored submissions grades from all the assignments. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with submissions grades. | ||||
|      */ | ||||
|     protected async getAllSubmissionsGrade(siteId?: string): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> { | ||||
|         return this.getAssignSubmissionsGradeFormatted(undefined, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all the stored submissions grades for a certain assignment. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with submissions grades. | ||||
|      */ | ||||
|     async getAssignSubmissionsGrade( | ||||
|         assignId: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> { | ||||
|         return this.getAssignSubmissionsGradeFormatted({ assignid: assignId }, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience helper function to get stored submissions grading formatted. | ||||
|      * | ||||
|      * @param conditions Query conditions. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with submissions grades. | ||||
|      */ | ||||
|     protected async getAssignSubmissionsGradeFormatted( | ||||
|         conditions: SQLiteDBRecordValues = {}, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const submissions: AddonModAssignSubmissionsGradingDBRecord[] = await db.getRecords(SUBMISSIONS_GRADES_TABLE, conditions); | ||||
| 
 | ||||
|         // Parse the plugin data and outcomes.
 | ||||
|         return submissions.map((submission) => ({ | ||||
|             assignid: submission.assignid, | ||||
|             userid: submission.userid, | ||||
|             courseid: submission.courseid, | ||||
|             grade: submission.grade, | ||||
|             attemptnumber: submission.attemptnumber, | ||||
|             addattempt: submission.addattempt, | ||||
|             workflowstate: submission.workflowstate, | ||||
|             applytoall: submission.applytoall, | ||||
|             outcomes: CoreTextUtils.instance.parseJSON<AddonModAssignOutcomes>(submission.outcomes, {}), | ||||
|             plugindata: CoreTextUtils.instance.parseJSON<AddonModAssignSavePluginData>(submission.plugindata, {}), | ||||
|             timemodified: submission.timemodified, | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a stored submission. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with submission. | ||||
|      */ | ||||
|     async getSubmission(assignId: number, userId?: number, siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted> { | ||||
|         userId = userId || CoreSites.instance.getCurrentSiteUserId(); | ||||
| 
 | ||||
|         const submissions = await this.getAssignSubmissionsFormatted({ assignid: assignId, userid: userId }, siteId); | ||||
| 
 | ||||
|         if (submissions.length) { | ||||
|             return submissions[0]; | ||||
|         } | ||||
| 
 | ||||
|         throw new CoreError('No records found.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the folder where to store files for an offline submission. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the path. | ||||
|      */ | ||||
|     async getSubmissionFolder(assignId: number, userId?: number, siteId?: string): Promise<string> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         userId = userId || site.getUserId(); | ||||
|         const siteFolderPath = CoreFile.instance.getSiteFolder(site.getId()); | ||||
|         const submissionFolderPath = 'offlineassign/' + assignId + '/' + userId; | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(siteFolderPath, submissionFolderPath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a stored submission grade. | ||||
|      * Submission grades are not identified using attempt number so it can retrieve the feedback for a previous attempt. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with submission grade. | ||||
|      */ | ||||
|     async getSubmissionGrade( | ||||
|         assignId: number, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted> { | ||||
|         userId = userId || CoreSites.instance.getCurrentSiteUserId(); | ||||
| 
 | ||||
|         const submissions = await this.getAssignSubmissionsGradeFormatted({ assignid: assignId, userid: userId }, siteId); | ||||
| 
 | ||||
|         if (submissions.length) { | ||||
|             return submissions[0]; | ||||
|         } | ||||
| 
 | ||||
|         throw new CoreError('No records found.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the folder where to store files for a certain plugin in an offline submission. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param pluginName Name of the plugin. Must be unique (both in submission and feedback plugins). | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the path. | ||||
|      */ | ||||
|     async getSubmissionPluginFolder(assignId: number, pluginName: string, userId?: number, siteId?: string): Promise<string> { | ||||
|         const folderPath = await this.getSubmissionFolder(assignId, userId, siteId); | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(folderPath, pluginName); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the assignment has something to be synced. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with boolean: whether the assignment has something to be synced. | ||||
|      */ | ||||
|     async hasAssignOfflineData(assignId: number, siteId?: string): Promise<boolean> { | ||||
|         const promises: | ||||
|         Promise<AddonModAssignSubmissionsDBRecordFormatted[] | AddonModAssignSubmissionsGradingDBRecordFormatted[]>[] = []; | ||||
| 
 | ||||
| 
 | ||||
|         promises.push(this.getAssignSubmissions(assignId, siteId)); | ||||
|         promises.push(this.getAssignSubmissionsGrade(assignId, siteId)); | ||||
| 
 | ||||
|         try { | ||||
|             const results = await Promise.all(promises); | ||||
| 
 | ||||
|             return results.some((result) => result.length); | ||||
|         } catch { | ||||
|             // No offline data found.
 | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Mark/Unmark a submission as being submitted. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param courseId Course ID the assign belongs to. | ||||
|      * @param submitted True to mark as submitted, false to mark as not submitted. | ||||
|      * @param acceptStatement True to accept the submission statement, false otherwise. | ||||
|      * @param timemodified The time the submission was last modified in online. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if marked, rejected if failure. | ||||
|      */ | ||||
|     async markSubmitted( | ||||
|         assignId: number, | ||||
|         courseId: number, | ||||
|         submitted: boolean, | ||||
|         acceptStatement: boolean, | ||||
|         timemodified: number, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<number> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         userId = userId || site.getUserId(); | ||||
|         let submission: AddonModAssignSubmissionsDBRecord; | ||||
|         try { | ||||
|             const savedSubmission: AddonModAssignSubmissionsDBRecordFormatted = | ||||
|                 await this.getSubmission(assignId, userId, site.getId()); | ||||
|             submission = Object.assign(savedSubmission, { | ||||
|                 plugindata: savedSubmission.plugindata ? JSON.stringify(savedSubmission.plugindata) : '{}', | ||||
|                 submitted: submitted ? 1 : 0, // Mark the submission.
 | ||||
|                 submissionstatement: acceptStatement ? 1 : 0, // Mark the submission.
 | ||||
|             }); | ||||
|         } catch { | ||||
|             // No submission, create an empty one.
 | ||||
|             const now = CoreTimeUtils.instance.timestamp(); | ||||
|             submission = { | ||||
|                 assignid: assignId, | ||||
|                 courseid: courseId, | ||||
|                 userid: userId, | ||||
|                 onlinetimemodified: timemodified, | ||||
|                 timecreated: now, | ||||
|                 timemodified: now, | ||||
|                 plugindata: '{}', | ||||
|                 submitted: submitted ? 1 : 0, // Mark the submission.
 | ||||
|                 submissionstatement: acceptStatement ? 1 : 0, // Mark the submission.
 | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         return await site.getDb().insertRecord(SUBMISSIONS_TABLE, submission); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save a submission to be sent later. | ||||
|      * | ||||
|      * @param assignId Assignment ID. | ||||
|      * @param courseId Course ID the assign belongs to. | ||||
|      * @param pluginData Data to save. | ||||
|      * @param timemodified The time the submission was last modified in online. | ||||
|      * @param submitted True if submission has been submitted, false otherwise. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     async saveSubmission( | ||||
|         assignId: number, | ||||
|         courseId: number, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         timemodified: number, | ||||
|         submitted: boolean, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<number> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         userId = userId || site.getUserId(); | ||||
| 
 | ||||
|         const now = CoreTimeUtils.instance.timestamp(); | ||||
|         const entry: AddonModAssignSubmissionsDBRecord = { | ||||
|             assignid: assignId, | ||||
|             courseid: courseId, | ||||
|             plugindata: pluginData ? JSON.stringify(pluginData) : '{}', | ||||
|             userid: userId, | ||||
|             submitted: submitted ? 1 : 0, | ||||
|             timecreated: now, | ||||
|             timemodified: now, | ||||
|             onlinetimemodified: timemodified, | ||||
|         }; | ||||
| 
 | ||||
|         return await site.getDb().insertRecord(SUBMISSIONS_TABLE, entry); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save a grading to be sent later. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param userId User ID. | ||||
|      * @param courseId Course ID the assign belongs to. | ||||
|      * @param grade Grade to submit. | ||||
|      * @param attemptNumber Number of the attempt being graded. | ||||
|      * @param addAttempt Admit the user to attempt again. | ||||
|      * @param workflowState Next workflow State. | ||||
|      * @param applyToAll If it's a team submission, whether the grade applies to all group members. | ||||
|      * @param outcomes Object including all outcomes values. If empty, any of them will be sent. | ||||
|      * @param pluginData Plugin data to save. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     async submitGradingForm( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         courseId: number, | ||||
|         grade: number, | ||||
|         attemptNumber: number, | ||||
|         addAttempt: boolean, | ||||
|         workflowState: string, | ||||
|         applyToAll: boolean, | ||||
|         outcomes: AddonModAssignOutcomes, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         siteId?: string, | ||||
|     ): Promise<number> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const now = CoreTimeUtils.instance.timestamp(); | ||||
|         const entry: AddonModAssignSubmissionsGradingDBRecord = { | ||||
|             assignid: assignId, | ||||
|             userid: userId, | ||||
|             courseid: courseId, | ||||
|             grade: grade, | ||||
|             attemptnumber: attemptNumber, | ||||
|             addattempt: addAttempt ? 1 : 0, | ||||
|             workflowstate: workflowState, | ||||
|             applytoall: applyToAll ? 1 : 0, | ||||
|             outcomes: outcomes ? JSON.stringify(outcomes) : '{}', | ||||
|             plugindata: pluginData ? JSON.stringify(pluginData) : '{}', | ||||
|             timemodified: now, | ||||
|         }; | ||||
| 
 | ||||
|         return await site.getDb().insertRecord(SUBMISSIONS_GRADES_TABLE, entry); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignOffline = makeSingleton(AddonModAssignOfflineProvider); | ||||
| 
 | ||||
| export type AddonModAssignSubmissionsDBRecordFormatted = Omit<AddonModAssignSubmissionsDBRecord, 'plugindata'> & { | ||||
|     plugindata: AddonModAssignSavePluginData; | ||||
| }; | ||||
| 
 | ||||
| export type AddonModAssignSubmissionsGradingDBRecordFormatted = | ||||
|     Omit<AddonModAssignSubmissionsGradingDBRecord, 'plugindata'|'outcomes'> & { | ||||
|         plugindata: AddonModAssignSavePluginData; | ||||
|         outcomes: AddonModAssignOutcomes; | ||||
|     }; | ||||
							
								
								
									
										572
									
								
								src/addons/mod/assign/services/assign-sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										572
									
								
								src/addons/mod/assign/services/assign-sync.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,572 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreEvents, CoreEventSiteData } from '@singletons/events'; | ||||
| import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | ||||
| import { CoreSyncBlockedError } from '@classes/base-sync'; | ||||
| import { | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssign, | ||||
|     AddonModAssignGetSubmissionStatusWSResponse, | ||||
|     AddonModAssignSubmissionStatusOptions, | ||||
| } from './assign'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync'; | ||||
| import { | ||||
|     AddonModAssignOffline, | ||||
|     AddonModAssignSubmissionsDBRecordFormatted, | ||||
|     AddonModAssignSubmissionsGradingDBRecordFormatted, | ||||
| } from './assign-offline'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { CoreCourseLogHelper } from '@features/course/services/log-helper'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreNetworkError } from '@classes/errors/network-error'; | ||||
| import { CoreGradesFormattedItem, CoreGradesFormattedRow, CoreGradesHelper } from '@features/grades/services/grades-helper'; | ||||
| import { AddonModAssignSubmissionDelegate } from './submission-delegate'; | ||||
| import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to sync assigns. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModAssignSyncResult> { | ||||
| 
 | ||||
|     static readonly AUTO_SYNCED = 'addon_mod_assign_autom_synced'; | ||||
|     static readonly MANUAL_SYNCED = 'addon_mod_assign_manual_synced'; | ||||
| 
 | ||||
|     protected componentTranslate: string; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super('AddonModLessonSyncProvider'); | ||||
|         this.componentTranslate = CoreCourse.instance.translateModuleName('assign'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the sync ID for a certain user grade. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param userId User the grade belongs to. | ||||
|      * @return Sync ID. | ||||
|      */ | ||||
|     getGradeSyncId(assignId: number, userId: number): string { | ||||
|         return 'assignGrade#' + assignId + '#' + userId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to get scale selected option. | ||||
|      * | ||||
|      * @param options Possible options. | ||||
|      * @param selected Selected option to search. | ||||
|      * @return Index of the selected option. | ||||
|      */ | ||||
|     protected getSelectedScaleId(options: string, selected: string): number { | ||||
|         let optionsList = options.split(','); | ||||
| 
 | ||||
|         optionsList = optionsList.map((value) => value.trim()); | ||||
| 
 | ||||
|         optionsList.unshift(''); | ||||
| 
 | ||||
|         const index = options.indexOf(selected) || 0; | ||||
|         if (index < 0) { | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         return index; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if an assignment has data to synchronize. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with boolean: whether it has data to sync. | ||||
|      */ | ||||
|     hasDataToSync(assignId: number, siteId?: string): Promise<boolean> { | ||||
|         return AddonModAssignOffline.instance.hasAssignOfflineData(assignId, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Try to synchronize all the assignments in a certain site or in all sites. | ||||
|      * | ||||
|      * @param siteId Site ID to sync. If not defined, sync all sites. | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     syncAllAssignments(siteId?: string, force?: boolean): Promise<void> { | ||||
|         return this.syncOnSites('all assignments', this.syncAllAssignmentsFunc.bind(this, !!force), siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync all assignments on a site. | ||||
|      * | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @param siteId Site ID to sync. If not defined, sync all sites. | ||||
|      * @param Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     protected async syncAllAssignmentsFunc(force: boolean, siteId: string): Promise<void> { | ||||
|         // Get all assignments that have offline data.
 | ||||
|         const assignIds = await AddonModAssignOffline.instance.getAllAssigns(siteId); | ||||
| 
 | ||||
|         // Try to sync all assignments.
 | ||||
|         await Promise.all(assignIds.map(async (assignId) => { | ||||
|             const result = force | ||||
|                 ? await this.syncAssign(assignId, siteId) | ||||
|                 : await this.syncAssignIfNeeded(assignId, siteId); | ||||
| 
 | ||||
|             if (result?.updated) { | ||||
|                 CoreEvents.trigger<AddonModAssignAutoSyncData>(AddonModAssignSyncProvider.AUTO_SYNCED, { | ||||
|                     assignId: assignId, | ||||
|                     warnings: result.warnings, | ||||
|                     gradesBlocked: result.gradesBlocked, | ||||
|                 }, siteId); | ||||
|             } | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync an assignment only if a certain time has passed since the last time. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when the assign is synced or it doesn't need to be synced. | ||||
|      */ | ||||
|     async syncAssignIfNeeded(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult | undefined> { | ||||
|         const needed = await this.isSyncNeeded(assignId, siteId); | ||||
| 
 | ||||
|         if (needed) { | ||||
|             return this.syncAssign(assignId, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Try to synchronize an assign. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved in success. | ||||
|      */ | ||||
|     async syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
|         this.componentTranslate = this.componentTranslate || CoreCourse.instance.translateModuleName('assign'); | ||||
| 
 | ||||
|         if (this.isSyncing(assignId, siteId)) { | ||||
|             // There's already a sync ongoing for this assign, return the promise.
 | ||||
|             return this.getOngoingSync(assignId, siteId)!; | ||||
|         } | ||||
| 
 | ||||
|         // Verify that assign isn't blocked.
 | ||||
|         if (CoreSync.instance.isBlocked(AddonModAssignProvider.COMPONENT, assignId, siteId)) { | ||||
|             this.logger.debug('Cannot sync assign ' + assignId + ' because it is blocked.'); | ||||
| 
 | ||||
|             throw new CoreSyncBlockedError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate })); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.logger.debug('Try to sync assign ' + assignId + ' in site ' + siteId); | ||||
| 
 | ||||
|         const syncPromise = this.performSyncAssign(assignId, siteId); | ||||
| 
 | ||||
|         return this.addOngoingSync(assignId, syncPromise, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the assign submission. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved in success. | ||||
|      */ | ||||
|     protected async performSyncAssign(assignId: number, siteId: string): Promise<AddonModAssignSyncResult> { | ||||
|         // Sync offline logs.
 | ||||
|         await CoreUtils.instance.ignoreErrors( | ||||
|             CoreCourseLogHelper.instance.syncActivity(AddonModAssignProvider.COMPONENT, assignId, siteId), | ||||
|         ); | ||||
| 
 | ||||
|         const result: AddonModAssignSyncResult = { | ||||
|             warnings: [], | ||||
|             updated: false, | ||||
|             gradesBlocked: [], | ||||
|         }; | ||||
| 
 | ||||
|         // Load offline data and sync offline logs.
 | ||||
|         const [submissions, grades] = await Promise.all([ | ||||
|             this.getOfflineSubmissions(assignId, siteId), | ||||
|             this.getOfflineGrades(assignId, siteId), | ||||
|         ]); | ||||
| 
 | ||||
|         if (!submissions.length && !grades.length) { | ||||
|             // Nothing to sync.
 | ||||
|             await CoreUtils.instance.ignoreErrors(this.setSyncTime(assignId, siteId)); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         if (!CoreApp.instance.isOnline()) { | ||||
|             // Cannot sync in offline.
 | ||||
|             throw new CoreNetworkError(); | ||||
|         } | ||||
| 
 | ||||
|         const courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; | ||||
| 
 | ||||
|         const assign = await AddonModAssign.instance.getAssignmentById(courseId, assignId, { siteId }); | ||||
| 
 | ||||
|         let promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises = promises.concat(submissions.map(async (submission) => { | ||||
|             await this.syncSubmission(assign, submission, result.warnings, siteId); | ||||
| 
 | ||||
|             result.updated = true; | ||||
| 
 | ||||
|             return; | ||||
|         })); | ||||
| 
 | ||||
|         promises = promises.concat(grades.map(async (grade) => { | ||||
|             try { | ||||
|                 await this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId); | ||||
| 
 | ||||
|                 result.updated = true; | ||||
|             } catch (error) { | ||||
|                 if (error instanceof CoreSyncBlockedError) { | ||||
|                     // Grade blocked, but allow finish the sync.
 | ||||
|                     result.gradesBlocked.push(grade.userid); | ||||
|                 } else { | ||||
|                     throw error; | ||||
|                 } | ||||
|             } | ||||
|         })); | ||||
| 
 | ||||
|         await CoreUtils.instance.allPromises(promises); | ||||
| 
 | ||||
|         if (result.updated) { | ||||
|             // Data has been sent to server. Now invalidate the WS calls.
 | ||||
|             await CoreUtils.instance.ignoreErrors(AddonModAssign.instance.invalidateContent(assign.cmid, courseId, siteId)); | ||||
|         } | ||||
| 
 | ||||
|         // Sync finished, set sync time.
 | ||||
|         await CoreUtils.instance.ignoreErrors(this.setSyncTime(assignId, siteId)); | ||||
| 
 | ||||
|         // All done, return the result.
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get offline grades to be sent. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise with grades. | ||||
|      */ | ||||
|     protected async getOfflineGrades( | ||||
|         assignId: number, | ||||
|         siteId: string, | ||||
|     ): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> { | ||||
|         // If no offline data found, return empty array.
 | ||||
|         return CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getAssignSubmissionsGrade(assignId, siteId), []); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get offline submissions to be sent. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise with submissions. | ||||
|      */ | ||||
|     protected async getOfflineSubmissions( | ||||
|         assignId: number, | ||||
|         siteId: string, | ||||
|     ): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> { | ||||
|         // If no offline data found, return empty array.
 | ||||
|         return CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getAssignSubmissions(assignId, siteId), []); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize a submission. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param offlineData Submission offline data. | ||||
|      * @param warnings List of warnings. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if success, rejected otherwise. | ||||
|      */ | ||||
|     protected async syncSubmission( | ||||
|         assign: AddonModAssignAssign, | ||||
|         offlineData: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         warnings: string[], | ||||
|         siteId: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const userId = offlineData.userid; | ||||
|         const pluginData = {}; | ||||
|         const options: AddonModAssignSubmissionStatusOptions = { | ||||
|             userId, | ||||
|             cmId: assign.cmid, | ||||
|             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|             siteId, | ||||
|         }; | ||||
| 
 | ||||
|         const status = await AddonModAssign.instance.getSubmissionStatus(assign.id, options); | ||||
| 
 | ||||
|         const submission = AddonModAssign.instance.getSubmissionObjectFromAttempt(assign, status.lastattempt); | ||||
| 
 | ||||
|         if (submission && submission.timemodified != offlineData.onlinetimemodified) { | ||||
|             // The submission was modified in Moodle, discard the submission.
 | ||||
|             this.addOfflineDataDeletedWarning( | ||||
|                 warnings, | ||||
|                 this.componentTranslate, | ||||
|                 assign.name, | ||||
|                 Translate.instance.instant('addon.mod_assign.warningsubmissionmodified'), | ||||
|             ); | ||||
| 
 | ||||
|             return this.deleteSubmissionData(assign, offlineData, submission, siteId); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             if (submission?.plugins) { | ||||
|                 // Prepare plugins data.
 | ||||
|                 await Promise.all(submission.plugins.map((plugin) => | ||||
|                     AddonModAssignSubmissionDelegate.instance.preparePluginSyncData( | ||||
|                         assign, | ||||
|                         submission, | ||||
|                         plugin, | ||||
|                         offlineData, | ||||
|                         pluginData, | ||||
|                         siteId, | ||||
|                     ))); | ||||
|             } | ||||
| 
 | ||||
|             // Now save the submission.
 | ||||
|             if (Object.keys(pluginData).length > 0) { | ||||
|                 await AddonModAssign.instance.saveSubmissionOnline(assign.id, pluginData, siteId); | ||||
|             } | ||||
| 
 | ||||
|             if (assign.submissiondrafts && offlineData.submitted) { | ||||
|                 // The user submitted the assign manually. Submit it for grading.
 | ||||
|                 await AddonModAssign.instance.submitForGradingOnline(assign.id, !!offlineData.submissionstatement, siteId); | ||||
|             } | ||||
| 
 | ||||
|             // Submission data sent, update cached data. No need to block the user for this.
 | ||||
|             AddonModAssign.instance.getSubmissionStatus(assign.id, options); | ||||
|         } catch (error) { | ||||
|             if (!error || !CoreUtils.instance.isWebServiceError(error)) { | ||||
|                 // Local error, reject.
 | ||||
|                 throw error; | ||||
|             } | ||||
| 
 | ||||
|             // A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
 | ||||
|             this.addOfflineDataDeletedWarning( | ||||
|                 warnings, | ||||
|                 this.componentTranslate, | ||||
|                 assign.name, | ||||
|                 CoreTextUtils.instance.getErrorMessageFromError(error) || '', | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         // Delete the offline data.
 | ||||
|         await this.deleteSubmissionData(assign, offlineData, submission, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete the submission offline data (not grades). | ||||
|      * | ||||
|      * @param assign Assign. | ||||
|      * @param submission Submission. | ||||
|      * @param offlineData Offline data. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async deleteSubmissionData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         offlineData: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         submission?: AddonModAssignSubmission, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         // Delete the offline data.
 | ||||
|         await AddonModAssignOffline.instance.deleteSubmission(assign.id, offlineData.userid, siteId); | ||||
| 
 | ||||
|         if (submission?.plugins){ | ||||
|             // Delete plugins data.
 | ||||
|             await Promise.all(submission.plugins.map((plugin) => | ||||
|                 AddonModAssignSubmissionDelegate.instance.deletePluginOfflineData( | ||||
|                     assign, | ||||
|                     submission, | ||||
|                     plugin, | ||||
|                     offlineData, | ||||
|                     siteId, | ||||
|                 ))); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize a submission grade. | ||||
|      * | ||||
|      * @param assign Assignment. | ||||
|      * @param offlineData Submission grade offline data. | ||||
|      * @param warnings List of warnings. | ||||
|      * @param courseId Course Id. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if success, rejected otherwise. | ||||
|      */ | ||||
|     protected async syncSubmissionGrade( | ||||
|         assign: AddonModAssignAssign, | ||||
|         offlineData: AddonModAssignSubmissionsGradingDBRecordFormatted, | ||||
|         warnings: string[], | ||||
|         courseId: number, | ||||
|         siteId: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const userId = offlineData.userid; | ||||
|         const syncId = this.getGradeSyncId(assign.id, userId); | ||||
|         const options: AddonModAssignSubmissionStatusOptions = { | ||||
|             userId, | ||||
|             cmId: assign.cmid, | ||||
|             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|             siteId, | ||||
|         }; | ||||
| 
 | ||||
|         // Check if this grade sync is blocked.
 | ||||
|         if (CoreSync.instance.isBlocked(AddonModAssignProvider.COMPONENT, syncId, siteId)) { | ||||
|             this.logger.error(`Cannot sync grade for assign ${assign.id} and user ${userId} because it is blocked.!!!!`); | ||||
| 
 | ||||
|             throw new CoreSyncBlockedError(Translate.instance.instant( | ||||
|                 'core.errorsyncblocked', | ||||
|                 { $a: Translate.instance.instant('addon.mod_assign.syncblockedusercomponent') }, | ||||
|             )); | ||||
|         } | ||||
| 
 | ||||
|         const status = await AddonModAssign.instance.getSubmissionStatus(assign.id, options); | ||||
| 
 | ||||
|         const timemodified = (status.feedback && (status.feedback.gradeddate || status.feedback.grade?.timemodified)) || 0; | ||||
| 
 | ||||
|         if (timemodified > offlineData.timemodified) { | ||||
|             // The submission grade was modified in Moodle, discard it.
 | ||||
|             this.addOfflineDataDeletedWarning( | ||||
|                 warnings, | ||||
|                 this.componentTranslate, | ||||
|                 assign.name, | ||||
|                 Translate.instance.instant('addon.mod_assign.warningsubmissiongrademodified'), | ||||
|             ); | ||||
| 
 | ||||
|             return AddonModAssignOffline.instance.deleteSubmissionGrade(assign.id, userId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         // If grade has been modified from gradebook, do not use offline.
 | ||||
|         const grades: CoreGradesFormattedItem[] | CoreGradesFormattedRow[] = | ||||
|             await CoreGradesHelper.instance.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true); | ||||
| 
 | ||||
|         const gradeInfo = await CoreCourse.instance.getModuleBasicGradeInfo(assign.cmid, siteId); | ||||
| 
 | ||||
|         // Override offline grade and outcomes based on the gradebook data.
 | ||||
|         grades.forEach((grade: CoreGradesFormattedItem | CoreGradesFormattedRow) => { | ||||
|             if ('gradedategraded' in  grade && (grade.gradedategraded || 0) >= offlineData.timemodified) { | ||||
|                 if (!grade.outcomeid && !grade.scaleid) { | ||||
|                     if (gradeInfo && gradeInfo.scale) { | ||||
|                         offlineData.grade = this.getSelectedScaleId(gradeInfo.scale, grade.grade || ''); | ||||
|                     } else { | ||||
|                         offlineData.grade = parseFloat(grade.grade || ''); | ||||
|                     } | ||||
|                 } else if (gradeInfo && grade.outcomeid && AddonModAssign.instance.isOutcomesEditEnabled() && gradeInfo.outcomes) { | ||||
|                     gradeInfo.outcomes.forEach((outcome, index) => { | ||||
|                         if (outcome.scale && grade.itemnumber == index) { | ||||
|                             offlineData.outcomes[grade.itemnumber] = this.getSelectedScaleId( | ||||
|                                 outcome.scale, | ||||
|                                 grade.grade || '', | ||||
|                             ); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         try { | ||||
|             // Now submit the grade.
 | ||||
|             await AddonModAssign.instance.submitGradingFormOnline( | ||||
|                 assign.id, | ||||
|                 userId, | ||||
|                 offlineData.grade, | ||||
|                 offlineData.attemptnumber, | ||||
|                 !!offlineData.addattempt, | ||||
|                 offlineData.workflowstate, | ||||
|                 !!offlineData.applytoall, | ||||
|                 offlineData.outcomes, | ||||
|                 offlineData.plugindata, | ||||
|                 siteId, | ||||
|             ); | ||||
| 
 | ||||
|             // Grades sent. Discard grades drafts.
 | ||||
|             let promises: Promise<void | AddonModAssignGetSubmissionStatusWSResponse>[] = []; | ||||
|             if (status.feedback && status.feedback.plugins) { | ||||
|                 promises = status.feedback.plugins.map((plugin) => | ||||
|                     AddonModAssignFeedbackDelegate.instance.discardPluginFeedbackData(assign.id, userId, plugin, siteId)); | ||||
|             } | ||||
| 
 | ||||
|             // Update cached data.
 | ||||
|             promises.push(AddonModAssign.instance.getSubmissionStatus(assign.id, options)); | ||||
| 
 | ||||
|             await CoreUtils.instance.allPromises(promises); | ||||
|         } catch (error) { | ||||
|             if (!error || !CoreUtils.instance.isWebServiceError(error)) { | ||||
|                 // Local error, reject.
 | ||||
|                 throw error; | ||||
|             } | ||||
| 
 | ||||
|             // A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
 | ||||
|             this.addOfflineDataDeletedWarning( | ||||
|                 warnings, | ||||
|                 this.componentTranslate, | ||||
|                 assign.name, | ||||
|                 CoreTextUtils.instance.getErrorMessageFromError(error) || '', | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         // Delete the offline data.
 | ||||
|         await AddonModAssignOffline.instance.deleteSubmissionGrade(assign.id, userId, siteId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignSync = makeSingleton(AddonModAssignSyncProvider); | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by a assign sync. | ||||
|  */ | ||||
| export type AddonModAssignSyncResult = { | ||||
|     warnings: string[]; // List of warnings.
 | ||||
|     updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | ||||
|     courseId?: number; // Course the assign belongs to (if known).
 | ||||
|     gradesBlocked: number[]; // Whether some grade couldn't be synced because it was blocked. UserId fields of the blocked grade.
 | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Data passed to AUTO_SYNCED event. | ||||
|  */ | ||||
| export type AddonModAssignAutoSyncData = CoreEventSiteData & { | ||||
|     assignId: number; | ||||
|     warnings: string[]; | ||||
|     gradesBlocked: number[]; // Whether some grade couldn't be synced because it was blocked. UserId fields of the blocked grade.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data passed to MANUAL_SYNCED event. | ||||
|  */ | ||||
| export type AddonModAssignManualSyncData = AddonModAssignAutoSyncData & { | ||||
|     context: string; | ||||
|     submitId?: number; | ||||
| }; | ||||
							
								
								
									
										1877
									
								
								src/addons/mod/assign/services/assign.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1877
									
								
								src/addons/mod/assign/services/assign.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										151
									
								
								src/addons/mod/assign/services/database/assign.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/addons/mod/assign/services/database/assign.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,151 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreSiteSchema } from '@services/sites'; | ||||
| 
 | ||||
| /** | ||||
|  * Database variables for AddonModAssignOfflineProvider. | ||||
|  */ | ||||
| export const SUBMISSIONS_TABLE = 'addon_mod_assign_submissions'; | ||||
| export const SUBMISSIONS_GRADES_TABLE = 'addon_mod_assign_submissions_grading'; | ||||
| export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { | ||||
|     name: 'AddonModAssignOfflineProvider', | ||||
|     version: 1, | ||||
|     tables: [ | ||||
|         { | ||||
|             name: SUBMISSIONS_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'assignid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'userid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'plugindata', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'onlinetimemodified', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timemodified', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'submitted', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'submissionstatement', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|             ], | ||||
|             primaryKeys: ['assignid', 'userid'], | ||||
|         }, | ||||
|         { | ||||
|             name: SUBMISSIONS_GRADES_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'assignid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'userid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'grade', | ||||
|                     type: 'REAL', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'attemptnumber', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'addattempt', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'workflowstate', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'applytoall', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'outcomes', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'plugindata', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timemodified', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|             ], | ||||
|             primaryKeys: ['assignid', 'userid'], | ||||
|         }, | ||||
|     ], | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data about assign submissions to sync. | ||||
|  */ | ||||
| export type AddonModAssignSubmissionsDBRecord = { | ||||
|     assignid: number; // Primary key.
 | ||||
|     userid: number; // Primary key.
 | ||||
|     courseid: number; | ||||
|     plugindata: string; | ||||
|     onlinetimemodified: number; | ||||
|     timecreated: number; | ||||
|     timemodified: number; | ||||
|     submitted: number; | ||||
|     submissionstatement?: number; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data about assign submission grades to sync. | ||||
|  */ | ||||
| export type AddonModAssignSubmissionsGradingDBRecord = { | ||||
|     assignid: number; // Primary key.
 | ||||
|     userid: number; // Primary key.
 | ||||
|     courseid: number; | ||||
|     grade: number; // Real.
 | ||||
|     attemptnumber: number; | ||||
|     addattempt: number; | ||||
|     workflowstate: string; | ||||
|     applytoall: number; | ||||
|     outcomes: string; | ||||
|     plugindata: string; | ||||
|     timemodified: number; | ||||
| }; | ||||
							
								
								
									
										386
									
								
								src/addons/mod/assign/services/feedback-delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										386
									
								
								src/addons/mod/assign/services/feedback-delegate.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,386 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; | ||||
| import { AddonModAssignDefaultFeedbackHandler } from './handlers/default-feedback'; | ||||
| import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { AddonModAssignSubmissionFormatted } from './assign-helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Interface that all feedback handlers must implement. | ||||
|  */ | ||||
| export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler { | ||||
| 
 | ||||
|     /** | ||||
|      * Name of the type of feedback the handler supports. E.g. 'file'. | ||||
|      */ | ||||
|     type: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Discard the draft data of the feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     discardDraft?(assignId: number, userId: number, siteId?: string): void | Promise<void>; | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the plugin data. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent?(plugin: AddonModAssignPlugin): Type<unknown> | undefined | Promise<Type<unknown> | undefined>; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Return the draft saved data of the feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Data (or promise resolved with the data). | ||||
|      */ | ||||
|     getDraft?( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         siteId?: string, | ||||
|     ): Record<string, unknown> | Promise<Record<string, unknown> | undefined> | undefined; | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): CoreWSExternalFile[] | Promise<CoreWSExternalFile[]>; | ||||
| 
 | ||||
|     /** | ||||
|      * Get a readable name to use for the plugin. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The plugin name. | ||||
|      */ | ||||
|     getPluginName?(plugin: AddonModAssignPlugin): string; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the feedback data has changed for this plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the feedback. | ||||
|      * @param userId User ID of the submission. | ||||
|      * @return Boolean (or promise resolved with boolean): whether the data has changed. | ||||
|      */ | ||||
|     hasDataChanged?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|         userId: number, | ||||
|     ): boolean | Promise<boolean>; | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the plugin has draft data stored. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Boolean or promise resolved with boolean: whether the plugin has draft data. | ||||
|      */ | ||||
|     hasDraftData?(assignId: number, userId: number, siteId?: string): boolean | Promise<boolean>; | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch any required data for the plugin. | ||||
|      * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     prefetch?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<void>; | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the draft data saved. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     prepareFeedbackData?( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<void>; | ||||
| 
 | ||||
|     /** | ||||
|      * Save draft data of the feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param data The data to save. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     saveDraft?( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         data: Record<string, unknown>, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<void>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Delegate to register plugins for assign feedback. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonModAssignFeedbackHandler> { | ||||
| 
 | ||||
|     protected handlerNameProperty = 'type'; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected defaultHandler: AddonModAssignDefaultFeedbackHandler, | ||||
|     ) { | ||||
|         super('AddonModAssignFeedbackDelegate', true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Discard the draft data of the feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async discardPluginFeedbackData( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
|         return await this.executeFunctionOnEnabled(plugin.type, 'discardDraft', [assignId, userId, siteId]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the component to use for a certain feedback plugin. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @return Promise resolved with the component to use, undefined if not found. | ||||
|      */ | ||||
|     async getComponentForPlugin(plugin: AddonModAssignPlugin): Promise<Type<unknown> | undefined> { | ||||
|         return await this.executeFunctionOnEnabled(plugin.type, 'getComponent', [plugin]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the draft saved data of the feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the draft data. | ||||
|      */ | ||||
|     async getPluginDraftData<T>( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<T | undefined> { | ||||
|         return await this.executeFunctionOnEnabled(plugin.type, 'getDraft', [assignId, userId, siteId]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     async getPluginFiles( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreWSExternalFile[]> { | ||||
|         const files: CoreWSExternalFile[] | undefined = | ||||
|             await this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]); | ||||
| 
 | ||||
|         return files || []; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a readable name to use for a certain feedback plugin. | ||||
|      * | ||||
|      * @param plugin Plugin to get the name for. | ||||
|      * @return Human readable name. | ||||
|      */ | ||||
|     getPluginName(plugin: AddonModAssignPlugin): string | undefined { | ||||
|         return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [plugin]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the feedback data has changed for a certain plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the feedback. | ||||
|      * @param userId User ID of the submission. | ||||
|      * @return Promise resolved with true if data has changed, resolved with false otherwise. | ||||
|      */ | ||||
|     async hasPluginDataChanged( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|         userId: number, | ||||
|     ): Promise<boolean | undefined> { | ||||
|         return await this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'hasDataChanged', | ||||
|             [assign, submission, plugin, inputData, userId], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the plugin has draft data stored. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with true if it has draft data. | ||||
|      */ | ||||
|     async hasPluginDraftData( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<boolean | undefined> { | ||||
|         return await this.executeFunctionOnEnabled(plugin.type, 'hasDraftData', [assignId, userId, siteId]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a feedback plugin is supported. | ||||
|      * | ||||
|      * @param pluginType Type of the plugin. | ||||
|      * @return Whether it's supported. | ||||
|      */ | ||||
|     isPluginSupported(pluginType: string): boolean { | ||||
|         return this.hasHandler(pluginType, true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch any required data for a feedback plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async prefetch( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
|         return await this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to submit for a certain feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when data has been gathered. | ||||
|      */ | ||||
|     async preparePluginFeedbackData( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         return await this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'prepareFeedbackData', | ||||
|             [assignId, userId, plugin, pluginData, siteId], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save draft data of the feedback plugin. | ||||
|      * | ||||
|      * @param assignId The assignment ID. | ||||
|      * @param userId User ID. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data to save. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when data has been saved. | ||||
|      */ | ||||
|     async saveFeedbackDraft( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
|         return await this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'saveDraft', | ||||
|             [assignId, userId, plugin, inputData, siteId], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignFeedbackDelegate = makeSingleton(AddonModAssignFeedbackDelegateService); | ||||
							
								
								
									
										138
									
								
								src/addons/mod/assign/services/handlers/default-feedback.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/addons/mod/assign/services/handlers/default-feedback.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,138 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { AddonModAssignPlugin } from '../assign'; | ||||
| import { AddonModAssignFeedbackHandler } from '../feedback-delegate'; | ||||
| 
 | ||||
| /** | ||||
|  * Default handler used when a feedback plugin doesn't have a specific implementation. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedbackHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignDefaultFeedbackHandler'; | ||||
|     type = 'default'; | ||||
| 
 | ||||
|     /** | ||||
|      * Discard the draft data of the feedback plugin. | ||||
|      * | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     discardDraft(): void { | ||||
|         // Nothing to do.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the draft saved data of the feedback plugin. | ||||
|      * | ||||
|      * @return Data (or promise resolved with the data). | ||||
|      */ | ||||
|     getDraft(): undefined { | ||||
|         // Nothing to do.
 | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @return The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles(): CoreWSExternalFile[] { | ||||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a readable name to use for the plugin. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The plugin name. | ||||
|      */ | ||||
|     getPluginName(plugin: AddonModAssignPlugin): string { | ||||
|         // Check if there's a translated string for the plugin.
 | ||||
|         const translationId = 'addon.mod_assign_feedback_' + plugin.type + '.pluginname'; | ||||
|         const translation = Translate.instance.instant(translationId); | ||||
| 
 | ||||
|         if (translationId != translation) { | ||||
|             // Translation found, use it.
 | ||||
|             return translation; | ||||
|         } | ||||
| 
 | ||||
|         // Fallback to WS string.
 | ||||
|         if (plugin.name) { | ||||
|             return plugin.name; | ||||
|         } | ||||
| 
 | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the feedback data has changed for this plugin. | ||||
|      * | ||||
|      * @return Boolean (or promise resolved with boolean): whether the data has changed. | ||||
|      */ | ||||
|     hasDataChanged(): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the plugin has draft data stored. | ||||
|      * | ||||
|      * @return Boolean or promise resolved with boolean: whether the plugin has draft data. | ||||
|      */ | ||||
|     hasDraftData(): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch any required data for the plugin. | ||||
|      * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async prefetch(): Promise<void> { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the draft data saved. | ||||
|      * | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     prepareFeedbackData(): void { | ||||
|         // Nothing to do.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save draft data of the feedback plugin. | ||||
|      * | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     saveDraft(): void { | ||||
|         // Nothing to do.
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										201
									
								
								src/addons/mod/assign/services/handlers/default-submission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/addons/mod/assign/services/handlers/default-submission.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,201 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { AddonModAssignPlugin } from '../assign'; | ||||
| import { AddonModAssignSubmissionHandler } from '../submission-delegate'; | ||||
| 
 | ||||
| /** | ||||
|  * Default handler used when a submission plugin doesn't have a specific implementation. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSubmissionHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignBaseSubmissionHandler'; | ||||
|     type = 'base'; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the | ||||
|      * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit | ||||
|      * unfiltered data. | ||||
|      * | ||||
|      * @return Boolean or promise resolved with boolean: whether it can be edited in offline. | ||||
|      */ | ||||
|     canEditOffline(): boolean | Promise<boolean> { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a plugin has no data. | ||||
|      * | ||||
|      * @return Whether the plugin is empty. | ||||
|      */ | ||||
|     isEmpty(): boolean { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Should clear temporary data for a cancelled submission. | ||||
|      */ | ||||
|     clearTmpData(): void { | ||||
|         // Nothing to do.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This function will be called when the user wants to create a new submission based on the previous one. | ||||
|      * It should add to pluginData the data to send to server based in the data in plugin (previous attempt). | ||||
|      * | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     copySubmissionData(): void { | ||||
|         // Nothing to do.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete any stored data for the plugin and submission. | ||||
|      * | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     deleteOfflineData(): void { | ||||
|         // Nothing to do.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @return The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles(): CoreWSExternalFile[] { | ||||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a readable name to use for the plugin. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The plugin name. | ||||
|      */ | ||||
|     getPluginName(plugin: AddonModAssignPlugin): string { | ||||
|         // Check if there's a translated string for the plugin.
 | ||||
|         const translationId = 'addon.mod_assign_submission_' + plugin.type + '.pluginname'; | ||||
|         const translation = Translate.instance.instant(translationId); | ||||
| 
 | ||||
|         if (translationId != translation) { | ||||
|             // Translation found, use it.
 | ||||
|             return translation; | ||||
|         } | ||||
| 
 | ||||
|         // Fallback to WS string.
 | ||||
|         if (plugin.name) { | ||||
|             return plugin.name; | ||||
|         } | ||||
| 
 | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to copy a previous submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The size (or promise resolved with size). | ||||
|      */ | ||||
|     getSizeForCopy(): number { | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to add or edit a submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The size (or promise resolved with size). | ||||
|      */ | ||||
|     getSizeForEdit(): number { | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the submission data has changed for this plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @return Boolean (or promise resolved with boolean): whether the data has changed. | ||||
|      */ | ||||
|     hasDataChanged(): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled for edit on a site level. | ||||
|      * | ||||
|      * @return Whether or not the handler is enabled for edit on a site level. | ||||
|      */ | ||||
|     isEnabledForEdit(): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch any required data for the plugin. | ||||
|      * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async prefetch(): Promise<void> { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the input data. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param offline Whether the user is editing in offline. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     prepareSubmissionData(): void { | ||||
|         // Nothing to do.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the offline data stored. | ||||
|      * This will be used when performing a synchronization. | ||||
|      * | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     prepareSyncData(): void { | ||||
|         // Nothing to do.
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										32
									
								
								src/addons/mod/assign/services/handlers/index-link.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/addons/mod/assign/services/handlers/index-link.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat links to assign index page. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignIndexLinkHandler'; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super('AddonModAssign', 'assign'); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignIndexLinkHandler = makeSingleton(AddonModAssignIndexLinkHandlerService); | ||||
							
								
								
									
										32
									
								
								src/addons/mod/assign/services/handlers/list-link.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/addons/mod/assign/services/handlers/list-link.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat links to assign list page. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignListLinkHandlerService extends CoreContentLinksModuleListHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignListLinkHandler'; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super('AddonModAssign', 'assign'); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignListLinkHandler = makeSingleton(AddonModAssignListLinkHandlerService); | ||||
							
								
								
									
										94
									
								
								src/addons/mod/assign/services/handlers/module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/addons/mod/assign/services/handlers/module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,94 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; | ||||
| import { AddonModAssignIndexComponent } from '../../components/index'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; | ||||
| import { CoreCourseModule } from '@features/course/services/course-helper'; | ||||
| import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; | ||||
| import { AddonModAssign } from '../assign'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to support assign modules. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignModuleHandlerService implements CoreCourseModuleHandler { | ||||
| 
 | ||||
|     static readonly PAGE_NAME = 'mod_assign'; | ||||
| 
 | ||||
|     name = 'AddonModAssign'; | ||||
|     modName = 'assign'; | ||||
| 
 | ||||
|     supportedFeatures = { | ||||
|         [CoreConstants.FEATURE_GROUPS]: true, | ||||
|         [CoreConstants.FEATURE_GROUPINGS]: true, | ||||
|         [CoreConstants.FEATURE_MOD_INTRO]: true, | ||||
|         [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, | ||||
|         [CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true, | ||||
|         [CoreConstants.FEATURE_GRADE_HAS_GRADE]: true, | ||||
|         [CoreConstants.FEATURE_GRADE_OUTCOMES]: true, | ||||
|         [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, | ||||
|         [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, | ||||
|         [CoreConstants.FEATURE_ADVANCED_GRADING]: true, | ||||
|         [CoreConstants.FEATURE_PLAGIARISM]: true, | ||||
|         [CoreConstants.FEATURE_COMMENT]: true, | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return Whether or not the handler is enabled on a site level. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return AddonModAssign.instance.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the data required to display the module in the course contents view. | ||||
|      * | ||||
|      * @param module The module object. | ||||
|      * @return Data to render the module. | ||||
|      */ | ||||
|     getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData { | ||||
|         return { | ||||
|             icon: CoreCourse.instance.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), | ||||
|             title: module.name, | ||||
|             class: 'addon-mod_assign-handler', | ||||
|             showDownloadButton: true, | ||||
|             action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void { | ||||
|                 options = options || {}; | ||||
|                 options.params = options.params || {}; | ||||
|                 Object.assign(options.params, { module }); | ||||
|                 const routeParams = '/' + courseId + '/' + module.id; | ||||
| 
 | ||||
|                 CoreNavigator.instance.navigateToSitePath(AddonModAssignModuleHandlerService.PAGE_NAME + routeParams, options); | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the component to render the module. This is needed to support singleactivity course format. | ||||
|      * The component returned must implement CoreCourseModuleMainComponent. | ||||
|      * | ||||
|      * @return The component to use, undefined if not found. | ||||
|      */ | ||||
|     async getMainComponent(): Promise<Type<unknown> | undefined> { | ||||
|         return AddonModAssignIndexComponent; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignModuleHandler = makeSingleton(AddonModAssignModuleHandlerService); | ||||
							
								
								
									
										531
									
								
								src/addons/mod/assign/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										531
									
								
								src/addons/mod/assign/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,531 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { | ||||
|     AddonModAssign, | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssignSubmissionStatusOptions, | ||||
| } from '../assign'; | ||||
| import { AddonModAssignSubmissionDelegate } from '../submission-delegate'; | ||||
| import { AddonModAssignFeedbackDelegate } from '../feedback-delegate'; | ||||
| import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; | ||||
| import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../assign-helper'; | ||||
| import { CoreCourseHelper } from '@features/course/services/course-helper'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreFilepool } from '@services/filepool'; | ||||
| import { CoreGroups } from '@services/groups'; | ||||
| import { AddonModAssignSync, AddonModAssignSyncResult } from '../assign-sync'; | ||||
| import { CoreUser } from '@features/user/services/user'; | ||||
| import { CoreGradesHelper } from '@features/grades/services/grades-helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to prefetch assigns. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { | ||||
| 
 | ||||
|     name = 'AddonModAssign'; | ||||
|     modName = 'assign'; | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     updatesNames = /^configuration$|^.*files$|^submissions$|^grades$|^gradeitems$|^outcomes$|^comments$/; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a certain module can use core_course_check_updates to check if it has updates. | ||||
|      * If not defined, it will assume all modules can be checked. | ||||
|      * The modules that return false will always be shown as outdated when they're downloaded. | ||||
|      * | ||||
|      * @param module Module. | ||||
|      * @param courseId Course ID the module belongs to. | ||||
|      * @return Whether the module can use check_updates. The promise should never be rejected. | ||||
|      */ | ||||
|     async canUseCheckUpdates(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> { | ||||
|         // Teachers cannot use the WS because it doesn't check student submissions.
 | ||||
|         try { | ||||
|             const assign = await AddonModAssign.instance.getAssignment(courseId, module.id); | ||||
| 
 | ||||
|             const data = await AddonModAssign.instance.getSubmissions(assign.id, { cmId: module.id }); | ||||
|             if (data.canviewsubmissions) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             // Check if the user can view their own submission.
 | ||||
|             await AddonModAssign.instance.getSubmissionStatus(assign.id, { cmId: module.id }); | ||||
| 
 | ||||
|             return true; | ||||
|         } catch { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get list of files. If not defined, we'll assume they're in module.contents. | ||||
|      * | ||||
|      * @param module Module. | ||||
|      * @param courseId Course ID the module belongs to. | ||||
|      * @return Promise resolved with the list of files. | ||||
|      */ | ||||
|     async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> { | ||||
|         const siteId = CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         try { | ||||
|             const assign = await AddonModAssign.instance.getAssignment(courseId, module.id, { siteId }); | ||||
|             // Get intro files and attachments.
 | ||||
|             let files = assign.introattachments || []; | ||||
|             files = files.concat(this.getIntroFilesFromInstance(module, assign)); | ||||
| 
 | ||||
|             // Now get the files in the submissions.
 | ||||
|             const submissionData = await AddonModAssign.instance.getSubmissions(assign.id, { cmId: module.id, siteId }); | ||||
| 
 | ||||
|             if (submissionData.canviewsubmissions) { | ||||
|                 // Teacher, get all submissions.
 | ||||
|                 const submissions = | ||||
|                     await AddonModAssignHelper.instance.getSubmissionsUserData(assign, submissionData.submissions, 0, { siteId }); | ||||
| 
 | ||||
|                 // Get all the files in the submissions.
 | ||||
|                 const promises = submissions.map((submission) => | ||||
|                     this.getSubmissionFiles(assign, submission.submitid!, !!submission.blindid, siteId).then((submissionFiles) => { | ||||
|                         files = files.concat(submissionFiles); | ||||
| 
 | ||||
|                         return; | ||||
|                     }).catch((error) => { | ||||
|                         if (error && error.errorcode == 'nopermission') { | ||||
|                             // The user does not have persmission to view this submission, ignore it.
 | ||||
|                             return; | ||||
|                         } | ||||
| 
 | ||||
|                         throw error; | ||||
|                     })); | ||||
| 
 | ||||
|                 await Promise.all(promises); | ||||
|             } else { | ||||
|                 // Student, get only his/her submissions.
 | ||||
|                 const userId = CoreSites.instance.getCurrentSiteUserId(); | ||||
|                 const blindMarking = !!assign.blindmarking && !assign.revealidentities; | ||||
| 
 | ||||
|                 const submissionFiles = await this.getSubmissionFiles(assign, userId, blindMarking, siteId); | ||||
|                 files = files.concat(submissionFiles); | ||||
|             } | ||||
| 
 | ||||
|             return files; | ||||
|         } catch { | ||||
|             // Error getting data, return empty list.
 | ||||
|             return []; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get submission files. | ||||
|      * | ||||
|      * @param assign Assign. | ||||
|      * @param submitId User ID of the submission to get. | ||||
|      * @param blindMarking True if blind marking, false otherwise. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with array of files. | ||||
|      */ | ||||
|     protected async getSubmissionFiles( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submitId: number, | ||||
|         blindMarking: boolean, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreWSExternalFile[]> { | ||||
| 
 | ||||
|         const submissionStatus = await AddonModAssign.instance.getSubmissionStatusWithRetry(assign, { | ||||
|             userId: submitId, | ||||
|             isBlind: blindMarking, | ||||
|             siteId, | ||||
|         }); | ||||
|         const userSubmission = AddonModAssign.instance.getSubmissionObjectFromAttempt(assign, submissionStatus.lastattempt); | ||||
| 
 | ||||
|         if (!submissionStatus.lastattempt || !userSubmission) { | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         const promises: Promise<CoreWSExternalFile[]>[] = []; | ||||
| 
 | ||||
|         if (userSubmission.plugins) { | ||||
|             // Add submission plugin files.
 | ||||
|             userSubmission.plugins.forEach((plugin) => { | ||||
|                 promises.push(AddonModAssignSubmissionDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId)); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         if (submissionStatus.feedback && submissionStatus.feedback.plugins) { | ||||
|             // Add feedback plugin files.
 | ||||
|             submissionStatus.feedback.plugins.forEach((plugin) => { | ||||
|                 promises.push(AddonModAssignFeedbackDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId)); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         const filesLists = await Promise.all(promises); | ||||
| 
 | ||||
|         return [].concat.apply([], filesLists); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate the prefetched content. | ||||
|      * | ||||
|      * @param moduleId The module ID. | ||||
|      * @param courseId The course ID the module belongs to. | ||||
|      * @return Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     async invalidateContent(moduleId: number, courseId: number): Promise<void> { | ||||
|         await AddonModAssign.instance.invalidateContent(moduleId, courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate WS calls needed to determine module status. | ||||
|      * | ||||
|      * @param module Module. | ||||
|      * @param courseId Course ID the module belongs to. | ||||
|      * @return Promise resolved when invalidated. | ||||
|      */ | ||||
|     async invalidateModule(module: CoreCourseAnyModuleData): Promise<void> { | ||||
|         return CoreCourse.instance.invalidateModule(module.id); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return AddonModAssign.instance.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch a module. | ||||
|      * | ||||
|      * @param module Module. | ||||
|      * @param courseId Course ID the module belongs to. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> { | ||||
|         return this.prefetchPackage(module, courseId, this.prefetchAssign.bind(this, module, courseId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch an assignment. | ||||
|      * | ||||
|      * @param module Module. | ||||
|      * @param courseId Course ID the module belongs to. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async prefetchAssign(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> { | ||||
|         const userId = CoreSites.instance.getCurrentSiteUserId(); | ||||
|         courseId = courseId || module.course || CoreSites.instance.getCurrentSiteHomeId(); | ||||
|         const siteId = CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const options: CoreSitesCommonWSOptions = { | ||||
|             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|             siteId, | ||||
|         }; | ||||
| 
 | ||||
|         const modOptions: CoreCourseCommonModWSOptions = { | ||||
|             cmId: module.id, | ||||
|             ...options, | ||||
|         }; | ||||
| 
 | ||||
|         // Get assignment to retrieve all its submissions.
 | ||||
|         const assign = await AddonModAssign.instance.getAssignment(courseId, module.id, options); | ||||
|         const promises: Promise<any>[] = []; | ||||
|         const blindMarking = assign.blindmarking && !assign.revealidentities; | ||||
| 
 | ||||
|         if (blindMarking) { | ||||
|             promises.push( | ||||
|                 CoreUtils.instance.ignoreErrors(AddonModAssign.instance.getAssignmentUserMappings(assign.id, -1, modOptions)), | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         promises.push(this.prefetchSubmissions(assign, courseId, module.id, userId, siteId)); | ||||
| 
 | ||||
|         promises.push(CoreCourseHelper.instance.getModuleCourseIdByInstance(assign.id, 'assign', siteId)); | ||||
| 
 | ||||
|         // Download intro files and attachments. Do not call getFiles because it'd call some WS twice.
 | ||||
|         let files = assign.introattachments || []; | ||||
|         files = files.concat(this.getIntroFilesFromInstance(module, assign)); | ||||
| 
 | ||||
|         promises.push(CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id)); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch assign submissions. | ||||
|      * | ||||
|      * @param assign Assign. | ||||
|      * @param courseId Course ID. | ||||
|      * @param moduleId Module ID. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when prefetched, rejected otherwise. | ||||
|      */ | ||||
|     protected async prefetchSubmissions( | ||||
|         assign: AddonModAssignAssign, | ||||
|         courseId: number, | ||||
|         moduleId: number, | ||||
|         userId: number, | ||||
|         siteId: string, | ||||
|     ): Promise<void> { | ||||
|         const modOptions: CoreCourseCommonModWSOptions = { | ||||
|             cmId: moduleId, | ||||
|             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|             siteId, | ||||
|         }; | ||||
| 
 | ||||
|         // Get submissions.
 | ||||
|         const submissions = await AddonModAssign.instance.getSubmissions(assign.id, modOptions); | ||||
|         const promises: Promise<any>[] = []; | ||||
| 
 | ||||
|         promises.push(this.prefetchParticipantSubmissions( | ||||
|             assign, | ||||
|             submissions.canviewsubmissions, | ||||
|             submissions.submissions, | ||||
|             moduleId, | ||||
|             courseId, | ||||
|             userId, | ||||
|             siteId, | ||||
|         )); | ||||
| 
 | ||||
|         // Prefetch own submission, we need to do this for teachers too so the response with error is cached.
 | ||||
|         promises.push( | ||||
|             this.prefetchSubmission( | ||||
|                 assign, | ||||
|                 courseId, | ||||
|                 moduleId, | ||||
|                 { | ||||
|                     userId, | ||||
|                     readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|                     siteId, | ||||
|                 }, | ||||
|                 true, | ||||
|             ), | ||||
|         ); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
|     } | ||||
| 
 | ||||
|     protected async prefetchParticipantSubmissions( | ||||
|         assign: AddonModAssignAssign, | ||||
|         canviewsubmissions: boolean, | ||||
|         submissions: AddonModAssignSubmission[] = [], | ||||
|         moduleId: number, | ||||
|         courseId: number, | ||||
|         userId: number, | ||||
|         siteId: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const options: CoreSitesCommonWSOptions = { | ||||
|             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|             siteId, | ||||
|         }; | ||||
| 
 | ||||
|         const modOptions: CoreCourseCommonModWSOptions = { | ||||
|             cmId: moduleId, | ||||
|             ...options, | ||||
|         }; | ||||
| 
 | ||||
|         // Always prefetch groupInfo.
 | ||||
|         const groupInfo = await CoreGroups.instance.getActivityGroupInfo(assign.cmid, false, undefined, siteId); | ||||
|         if (!canviewsubmissions) { | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Teacher, prefetch all submissions.
 | ||||
|         if (!groupInfo.groups || groupInfo.groups.length == 0) { | ||||
|             groupInfo.groups = [{ id: 0, name: '' }]; | ||||
|         } | ||||
| 
 | ||||
|         const promises = groupInfo.groups.map((group) => | ||||
|             AddonModAssignHelper.instance.getSubmissionsUserData(assign, submissions, group.id, options) | ||||
|                 .then((submissions: AddonModAssignSubmissionFormatted[]) => { | ||||
| 
 | ||||
|                     const subPromises: Promise<any>[] = submissions.map((submission) => { | ||||
|                         const submissionOptions = { | ||||
|                             userId: submission.submitid, | ||||
|                             groupId: group.id, | ||||
|                             isBlind: !!submission.blindid, | ||||
|                             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|                             siteId, | ||||
|                         }; | ||||
| 
 | ||||
|                         return this.prefetchSubmission(assign, courseId, moduleId, submissionOptions, true); | ||||
|                     }); | ||||
| 
 | ||||
|                     if (!assign.markingworkflow) { | ||||
|                         // Get assignment grades only if workflow is not enabled to check grading date.
 | ||||
|                         subPromises.push(AddonModAssign.instance.getAssignmentGrades(assign.id, modOptions)); | ||||
|                     } | ||||
| 
 | ||||
|                     // Prefetch the submission of the current user even if it does not exist, this will be create it.
 | ||||
|                     if (!submissions || !submissions.find((subm: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) { | ||||
|                         const submissionOptions = { | ||||
|                             userId, | ||||
|                             groupId: group.id, | ||||
|                             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|                             siteId, | ||||
|                         }; | ||||
| 
 | ||||
|                         subPromises.push(this.prefetchSubmission(assign, courseId, moduleId, submissionOptions)); | ||||
|                     } | ||||
| 
 | ||||
|                     return Promise.all(subPromises); | ||||
|                 }).then(async () => { | ||||
|                     // Participiants already fetched, we don't need to ignore cache now.
 | ||||
|                     const participants = await AddonModAssignHelper.instance.getParticipants(assign, group.id, { siteId }); | ||||
| 
 | ||||
|                     // Fail silently (Moodle < 3.2).
 | ||||
|                     await CoreUtils.instance.ignoreErrors( | ||||
|                         CoreUser.instance.prefetchUserAvatars(participants, 'profileimageurl', siteId), | ||||
|                     ); | ||||
| 
 | ||||
|                     return; | ||||
|                 })); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch a submission. | ||||
|      * | ||||
|      * @param assign Assign. | ||||
|      * @param courseId Course ID. | ||||
|      * @param moduleId Module ID. | ||||
|      * @param options Other options, see getSubmissionStatusWithRetry. | ||||
|      * @param resolveOnNoPermission If true, will avoid throwing if a nopermission error is raised. | ||||
|      * @return Promise resolved when prefetched, rejected otherwise. | ||||
|      */ | ||||
|     protected async prefetchSubmission( | ||||
|         assign: AddonModAssignAssign, | ||||
|         courseId: number, | ||||
|         moduleId: number, | ||||
|         options: AddonModAssignSubmissionStatusOptions = {}, | ||||
|         resolveOnNoPermission = false, | ||||
|     ): Promise<void> { | ||||
|         const submission = await AddonModAssign.instance.getSubmissionStatusWithRetry(assign, options); | ||||
|         const siteId = options.siteId!; | ||||
|         const userId = options.userId; | ||||
| 
 | ||||
|         try { | ||||
|             const promises: Promise<any>[] = []; | ||||
|             const blindMarking = !!assign.blindmarking && !assign.revealidentities; | ||||
|             let userIds: number[] = []; | ||||
|             const userSubmission = AddonModAssign.instance.getSubmissionObjectFromAttempt(assign, submission.lastattempt); | ||||
| 
 | ||||
|             if (submission.lastattempt) { | ||||
|                 // Get IDs of the members who need to submit.
 | ||||
|                 if (!blindMarking && submission.lastattempt.submissiongroupmemberswhoneedtosubmit) { | ||||
|                     userIds = userIds.concat(submission.lastattempt.submissiongroupmemberswhoneedtosubmit); | ||||
|                 } | ||||
| 
 | ||||
|                 if (userSubmission && userSubmission.id) { | ||||
|                     // Prefetch submission plugins data.
 | ||||
|                     if (userSubmission.plugins) { | ||||
|                         userSubmission.plugins.forEach((plugin) => { | ||||
|                             // Prefetch the plugin WS data.
 | ||||
|                             promises.push( | ||||
|                                 AddonModAssignSubmissionDelegate.instance.prefetch(assign, userSubmission, plugin, siteId), | ||||
|                             ); | ||||
| 
 | ||||
|                             // Prefetch the plugin files.
 | ||||
|                             promises.push( | ||||
|                                 AddonModAssignSubmissionDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId) | ||||
|                                     .then((files) => | ||||
|                                         CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id)) | ||||
|                                     .catch(() => { | ||||
|                                         // Ignore errors.
 | ||||
|                                     }), | ||||
|                             ); | ||||
|                         }); | ||||
|                     } | ||||
| 
 | ||||
|                     // Get ID of the user who did the submission.
 | ||||
|                     if (userSubmission.userid) { | ||||
|                         userIds.push(userSubmission.userid); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Prefetch grade items.
 | ||||
|             if (userId) { | ||||
|                 promises.push(CoreCourse.instance.getModuleBasicGradeInfo(moduleId, siteId).then((gradeInfo) => { | ||||
|                     if (gradeInfo) { | ||||
|                         promises.push( | ||||
|                             CoreGradesHelper.instance.getGradeModuleItems(courseId, moduleId, userId, undefined, siteId, true), | ||||
|                         ); | ||||
|                     } | ||||
| 
 | ||||
|                     return; | ||||
|                 })); | ||||
|             } | ||||
| 
 | ||||
|             // Prefetch feedback.
 | ||||
|             if (submission.feedback) { | ||||
|                 // Get profile and image of the grader.
 | ||||
|                 if (submission.feedback.grade && submission.feedback.grade.grader > 0) { | ||||
|                     userIds.push(submission.feedback.grade.grader); | ||||
|                 } | ||||
| 
 | ||||
|                 // Prefetch feedback plugins data.
 | ||||
|                 if (submission.feedback.plugins && userSubmission && userSubmission.id) { | ||||
|                     submission.feedback.plugins.forEach((plugin) => { | ||||
|                         // Prefetch the plugin WS data.
 | ||||
|                         promises.push(AddonModAssignFeedbackDelegate.instance.prefetch(assign, userSubmission, plugin, siteId)); | ||||
| 
 | ||||
|                         // Prefetch the plugin files.
 | ||||
|                         promises.push( | ||||
|                             AddonModAssignFeedbackDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId) | ||||
|                                 .then((files) => CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id)) | ||||
|                                 .catch(() => { | ||||
|                                     // Ignore errors.
 | ||||
|                                 }), | ||||
|                         ); | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Prefetch user profiles.
 | ||||
|             promises.push(CoreUser.instance.prefetchProfiles(userIds, courseId, siteId)); | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
|         } catch (error) { | ||||
|             // Ignore if the user can't view their own submission.
 | ||||
|             if (resolveOnNoPermission && error.errorcode != 'nopermission') { | ||||
|                 throw error; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync a module. | ||||
|      * | ||||
|      * @param module Module. | ||||
|      * @param courseId Course ID the module belongs to | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModAssignSyncResult> { | ||||
|         return AddonModAssignSync.instance.syncAssign(module.instance!, siteId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignPrefetchHandler = makeSingleton(AddonModAssignPrefetchHandlerService); | ||||
							
								
								
									
										66
									
								
								src/addons/mod/assign/services/handlers/push-click.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/addons/mod/assign/services/handlers/push-click.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCourseHelper } from '@features/course/services/course-helper'; | ||||
| import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; | ||||
| import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonModAssign } from '../assign'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for assign push notifications clicks. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignPushClickHandlerService implements CorePushNotificationsClickHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignPushClickHandler'; | ||||
|     priority = 200; | ||||
|     featureName = 'CoreCourseModuleDelegate_AddonModAssign'; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a notification click is handled by this handler. | ||||
|      * | ||||
|      * @param notification The notification to check. | ||||
|      * @return Whether the notification click is handled by this handler | ||||
|      */ | ||||
|     async handles(notification: NotificationData): Promise<boolean> { | ||||
|         return CoreUtils.instance.isTrueOrOne(notification.notif) && notification.moodlecomponent == 'mod_assign' && | ||||
|                 notification.name == 'assign_notification'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle the notification click. | ||||
|      * | ||||
|      * @param notification The notification to check. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async handleClick(notification: NotificationData): Promise<void> { | ||||
|         const contextUrlParams = CoreUrlUtils.instance.extractUrlParams(notification.contexturl); | ||||
|         const courseId = Number(notification.courseid); | ||||
|         const moduleId = Number(contextUrlParams.id); | ||||
| 
 | ||||
|         await CoreUtils.instance.ignoreErrors(AddonModAssign.instance.invalidateContent(moduleId, courseId, notification.site)); | ||||
|         await CoreCourseHelper.instance.navigateToModule(moduleId, notification.site, courseId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignPushClickHandler = makeSingleton(AddonModAssignPushClickHandlerService); | ||||
| 
 | ||||
| type NotificationData = CorePushNotificationsNotificationBasicData & { | ||||
|     courseid: number; | ||||
|     contexturl: string; | ||||
| }; | ||||
							
								
								
									
										50
									
								
								src/addons/mod/assign/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/addons/mod/assign/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCronHandler } from '@services/cron'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonModAssignSync } from '../assign-sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Synchronization cron handler. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignSyncCronHandlerService implements CoreCronHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignSyncCronHandler'; | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the process. | ||||
|      * Receives the ID of the site affected, undefined for all sites. | ||||
|      * | ||||
|      * @param siteId ID of the site affected, undefined for all sites. | ||||
|      * @param force Wether the execution is forced (manual sync). | ||||
|      * @return Promise resolved when done, rejected if failure. | ||||
|      */ | ||||
|     execute(siteId?: string, force?: boolean): Promise<void> { | ||||
|         return AddonModAssignSync.instance.syncAllAssignments(siteId, force); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the time between consecutive executions. | ||||
|      * | ||||
|      * @return Time between consecutive executions (in ms). | ||||
|      */ | ||||
|     getInterval(): number { | ||||
|         return AddonModAssignSync.instance.syncInterval; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignSyncCronHandler = makeSingleton(AddonModAssignSyncCronHandlerService); | ||||
							
								
								
									
										566
									
								
								src/addons/mod/assign/services/submission-delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										566
									
								
								src/addons/mod/assign/services/submission-delegate.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,566 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; | ||||
| import { AddonModAssignDefaultSubmissionHandler } from './handlers/default-submission'; | ||||
| import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { AddonModAssignSubmissionsDBRecordFormatted } from './assign-offline'; | ||||
| 
 | ||||
| /** | ||||
|  * Interface that all submission handlers must implement. | ||||
|  */ | ||||
| export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { | ||||
| 
 | ||||
|     /** | ||||
|      * Name of the type of submission the handler supports. E.g. 'file'. | ||||
|      */ | ||||
|     type: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the | ||||
|      * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit | ||||
|      * unfiltered data. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return Boolean or promise resolved with boolean: whether it can be edited in offline. | ||||
|      */ | ||||
|     canEditOffline?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): boolean | Promise<boolean>; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a plugin has no data. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return Whether the plugin is empty. | ||||
|      */ | ||||
|     isEmpty?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): boolean; | ||||
| 
 | ||||
|     /** | ||||
|      * Should clear temporary data for a cancelled submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      */ | ||||
|     clearTmpData?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): void; | ||||
| 
 | ||||
|     /** | ||||
|      * This function will be called when the user wants to create a new submission based on the previous one. | ||||
|      * It should add to pluginData the data to send to server based in the data in plugin (previous attempt). | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     copySubmissionData?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<void>; | ||||
| 
 | ||||
|     /** | ||||
|      * Delete any stored data for the plugin and submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param offlineData Offline data stored. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     deleteOfflineData?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         offlineData: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<void>; | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the plugin data, either in read or in edit mode. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @param edit Whether the user is editing. | ||||
|      * @return The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent?( | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         edit?: boolean, | ||||
|     ): Type<unknown> | undefined | Promise<Type<unknown> | undefined>; | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): CoreWSExternalFile[] | Promise<CoreWSExternalFile[]>; | ||||
| 
 | ||||
|     /** | ||||
|      * Get a readable name to use for the plugin. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The plugin name. | ||||
|      */ | ||||
|     getPluginName?(plugin: AddonModAssignPlugin): string; | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to copy a previous submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The size (or promise resolved with size). | ||||
|      */ | ||||
|     getSizeForCopy?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): number | Promise<number>; | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to add or edit a submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @return The size (or promise resolved with size). | ||||
|      */ | ||||
|     getSizeForEdit?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): number | Promise<number>; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the submission data has changed for this plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @return Boolean (or promise resolved with boolean): whether the data has changed. | ||||
|      */ | ||||
|     hasDataChanged?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): boolean | Promise<boolean>; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled for edit on a site level. | ||||
|      * | ||||
|      * @return Whether or not the handler is enabled for edit on a site level. | ||||
|      */ | ||||
|     isEnabledForEdit?(): boolean | Promise<boolean>; | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch any required data for the plugin. | ||||
|      * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     prefetch?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<void>; | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the input data. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param offline Whether the user is editing in offline. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     prepareSubmissionData?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         offline?: boolean, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<void>; | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the offline data stored. | ||||
|      * This will be used when performing a synchronization. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param offlineData Offline data stored. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     prepareSyncData?( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         offlineData: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<void>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Delegate to register plugins for assign submission. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonModAssignSubmissionHandler> { | ||||
| 
 | ||||
|     protected handlerNameProperty = 'type'; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected defaultHandler: AddonModAssignDefaultSubmissionHandler, | ||||
|     ) { | ||||
|         super('AddonModAssignSubmissionDelegate', true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the plugin can be edited in offline for existing submissions. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return Promise resolved with boolean: whether it can be edited in offline. | ||||
|      */ | ||||
|     async canPluginEditOffline( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): Promise<boolean | undefined> { | ||||
|         return await this.executeFunctionOnEnabled(plugin.type, 'canEditOffline', [assign, submission, plugin]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Clear some temporary data for a certain plugin because a submission was cancelled. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      */ | ||||
|     clearTmpData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): void { | ||||
|         return this.executeFunctionOnEnabled(plugin.type, 'clearTmpData', [assign, submission, plugin, inputData]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Copy the data from last submitted attempt to the current submission for a certain plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when the data has been copied. | ||||
|      */ | ||||
|     async copyPluginSubmissionData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<void | undefined> { | ||||
|         return await this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'copySubmissionData', | ||||
|             [assign, plugin, pluginData, userId, siteId], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete offline data stored for a certain submission and plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param offlineData Offline data stored. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deletePluginOfflineData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         offlineData: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
|         return await this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'deleteOfflineData', | ||||
|             [assign, submission, plugin, offlineData, siteId], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the component to use for a certain submission plugin. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @param edit Whether the user is editing. | ||||
|      * @return Promise resolved with the component to use, undefined if not found. | ||||
|      */ | ||||
|     async getComponentForPlugin(plugin: AddonModAssignPlugin, edit?: boolean): Promise<Type<unknown> | undefined> { | ||||
|         return await this.executeFunctionOnEnabled(plugin.type, 'getComponent', [plugin, edit]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     async getPluginFiles( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreWSExternalFile[]> { | ||||
|         const files: CoreWSExternalFile[] | undefined = | ||||
|             await this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]); | ||||
| 
 | ||||
|         return files || []; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a readable name to use for a certain submission plugin. | ||||
|      * | ||||
|      * @param plugin Plugin to get the name for. | ||||
|      * @return Human readable name. | ||||
|      */ | ||||
|     getPluginName(plugin: AddonModAssignPlugin): string | undefined { | ||||
|         return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [plugin]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to copy a previous submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return Promise resolved with size. | ||||
|      */ | ||||
|     async getPluginSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise<number | undefined> { | ||||
|         return await this.executeFunctionOnEnabled(plugin.type, 'getSizeForCopy', [assign, plugin]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to add or edit a submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @return Promise resolved with size. | ||||
|      */ | ||||
|     async getPluginSizeForEdit( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): Promise<number | undefined> { | ||||
|         return await this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'getSizeForEdit', | ||||
|             [assign, submission, plugin, inputData], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the submission data has changed for a certain plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @return Promise resolved with true if data has changed, resolved with false otherwise. | ||||
|      */ | ||||
|     async hasPluginDataChanged( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): Promise<boolean | undefined> { | ||||
|         return await this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'hasDataChanged', | ||||
|             [assign, submission, plugin, inputData], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a submission plugin is supported. | ||||
|      * | ||||
|      * @param pluginType Type of the plugin. | ||||
|      * @return Whether it's supported. | ||||
|      */ | ||||
|     isPluginSupported(pluginType: string): boolean { | ||||
|         return this.hasHandler(pluginType, true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a submission plugin is supported for edit. | ||||
|      * | ||||
|      * @param pluginType Type of the plugin. | ||||
|      * @return Whether it's supported for edit. | ||||
|      */ | ||||
|     async isPluginSupportedForEdit(pluginType: string): Promise<boolean | undefined> { | ||||
|         return await this.executeFunctionOnEnabled(pluginType, 'isEnabledForEdit'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a plugin has no data. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return Whether the plugin is empty. | ||||
|      */ | ||||
|     isPluginEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean | undefined { | ||||
|         return this.executeFunctionOnEnabled(plugin.type, 'isEmpty', [assign, plugin]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch any required data for a submission plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async prefetch( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
|         return await this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to submit for a certain submission plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param offline Whether the user is editing in offline. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when data has been gathered. | ||||
|      */ | ||||
|     async preparePluginSubmissionData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: Record<string, unknown>, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         offline?: boolean, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<void | undefined> { | ||||
| 
 | ||||
|         return await this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'prepareSubmissionData', | ||||
|             [assign, submission, plugin, inputData, pluginData, offline, userId, siteId], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to server to synchronize an offline submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param offlineData Offline data stored. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when data has been gathered. | ||||
|      */ | ||||
|     async preparePluginSyncData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         offlineData: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         pluginData: AddonModAssignSavePluginData, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         return this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'prepareSyncData', | ||||
|             [assign, submission, plugin, offlineData, pluginData, siteId], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignSubmissionDelegate = makeSingleton(AddonModAssignSubmissionDelegateService); | ||||
							
								
								
									
										47
									
								
								src/addons/mod/assign/submission/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/addons/mod/assign/submission/comments/comments.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { AddonModAssignSubmissionCommentsHandler } from './services/handler'; | ||||
| import { AddonModAssignSubmissionCommentsComponent } from './component/comments'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate'; | ||||
| import { CoreCommentsComponentsModule } from '@features/comments/components/components.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModAssignSubmissionCommentsComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|         CoreCommentsComponentsModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionCommentsHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModAssignSubmissionCommentsComponent, | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonModAssignSubmissionCommentsComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignSubmissionCommentsModule {} | ||||
| @ -0,0 +1,8 @@ | ||||
| <ion-item *ngIf="commentsEnabled" class="ion-text-wrap" (click)="showComments($event)" detail="false"> | ||||
|     <ion-label> | ||||
|         <h2>{{plugin.name}}</h2> | ||||
|         <core-comments contextLevel="module" [instanceId]="assign.cmid" component="assignsubmission_comments" | ||||
|             [itemId]="submission.id" area="submission_comments" [title]="plugin.name" [courseId]="assign.course"> | ||||
|         </core-comments> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
| @ -0,0 +1,61 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, ViewChild } from '@angular/core'; | ||||
| import { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin'; | ||||
| import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments'; | ||||
| import { CoreComments } from '@features/comments/services/comments'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a comments submission plugin. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-submission-comments', | ||||
|     templateUrl: 'addon-mod-assign-submission-comments.html', | ||||
| }) | ||||
| export class AddonModAssignSubmissionCommentsComponent extends AddonModAssignSubmissionPluginComponent { | ||||
| 
 | ||||
|     @ViewChild(CoreCommentsCommentsComponent) commentsComponent!: CoreCommentsCommentsComponent; | ||||
| 
 | ||||
|     commentsEnabled: boolean; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(); | ||||
| 
 | ||||
|         this.commentsEnabled = !CoreComments.instance.areCommentsDisabledInSite(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate the data. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     invalidate(): Promise<void> { | ||||
|         return CoreComments.instance.invalidateCommentsData( | ||||
|             'module', | ||||
|             this.assign.cmid, | ||||
|             'assignsubmission_comments', | ||||
|             this.submission.id, | ||||
|             'submission_comments', | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show the comments. | ||||
|      */ | ||||
|     showComments(e?: Event): void { | ||||
|         this.commentsComponent?.openComments(e); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/submission/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/submission/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| { | ||||
|     "pluginname": "Submission comments" | ||||
| } | ||||
							
								
								
									
										107
									
								
								src/addons/mod/assign/submission/comments/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/addons/mod/assign/submission/comments/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from '@addons/mod/assign/services/assign'; | ||||
| import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreComments } from '@features/comments/services/comments'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonModAssignSubmissionCommentsComponent } from '../component/comments'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for comments submission plugin. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' }) | ||||
| export class AddonModAssignSubmissionCommentsHandlerService implements AddonModAssignSubmissionHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignSubmissionCommentsHandler'; | ||||
|     type = 'comments'; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the | ||||
|      * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit | ||||
|      * unfiltered data. | ||||
|      * | ||||
|      * @return Boolean or promise resolved with boolean: whether it can be edited in offline. | ||||
|      */ | ||||
|     canEditOffline(): boolean { | ||||
|         // This plugin is read only, but return true to prevent blocking the edition.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the plugin data, either in read or in edit mode. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @param edit Whether the user is editing. | ||||
|      * @return The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent(plugin: AddonModAssignPlugin, edit = false): Type<unknown> | undefined { | ||||
|         return edit ? undefined : AddonModAssignSubmissionCommentsComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled for edit on a site level. | ||||
|      * | ||||
|      * @return Whether or not the handler is enabled for edit on a site level. | ||||
|      */ | ||||
|     isEnabledForEdit(): boolean{ | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch any required data for the plugin. | ||||
|      * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async prefetch( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         // Fail silently (Moodle < 3.1.1, 3.2)
 | ||||
|         await CoreUtils.instance.ignoreErrors( | ||||
|             CoreComments.instance.getComments( | ||||
|                 'module', | ||||
|                 assign.cmid, | ||||
|                 'assignsubmission_comments', | ||||
|                 submission.id, | ||||
|                 'submission_comments', | ||||
|                 0, | ||||
|                 siteId, | ||||
|             ), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignSubmissionCommentsHandler = makeSingleton(AddonModAssignSubmissionCommentsHandlerService); | ||||
| @ -0,0 +1,19 @@ | ||||
| <!-- Read only. --> | ||||
| <ion-item class="ion-text-wrap" *ngIf="files && files.length && !edit"> | ||||
|     <ion-label> | ||||
|         <h2>{{ plugin.name }}</h2> | ||||
|         <div lines="none"> | ||||
|             <core-files [files]="files" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-files> | ||||
|         </div> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
| 
 | ||||
| <!-- Edit --> | ||||
| <div *ngIf="edit"> | ||||
|     <ion-item-divider class="ion-text-wrap" sticky="true"> | ||||
|         <ion-label><h2>{{ plugin.name }}</h2></ion-label> | ||||
|     </ion-item-divider> | ||||
|     <core-attachments [files]="files" [maxSize]="maxSize" [maxSubmissions]="maxSubmissions" | ||||
|         [component]="component" [componentId]="assign.cmid" [acceptedTypes]="acceptedTypes" [allowOffline]="allowOffline"> | ||||
|     </core-attachments> | ||||
| </div> | ||||
							
								
								
									
										85
									
								
								src/addons/mod/assign/submission/file/component/file.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/addons/mod/assign/submission/file/component/file.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin'; | ||||
| import { AddonModAssign, AddonModAssignProvider } from '@addons/mod/assign/services/assign'; | ||||
| import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper'; | ||||
| import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline'; | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; | ||||
| import { CoreFileSession } from '@services/file-session'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { AddonModAssignSubmissionFileHandlerService } from '../services/handler'; | ||||
| import { FileEntry } from '@ionic-native/file/ngx'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a file submission plugin. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-submission-file', | ||||
|     templateUrl: 'addon-mod-assign-submission-file.html', | ||||
| }) | ||||
| export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmissionPluginComponent implements OnInit { | ||||
| 
 | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
| 
 | ||||
|     maxSize?: number; | ||||
|     acceptedTypes?: string; | ||||
|     maxSubmissions?: number; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         // Get the offline data.
 | ||||
|         const filesData = await CoreUtils.instance.ignoreErrors( | ||||
|             AddonModAssignOffline.instance.getSubmission(this.assign.id), | ||||
|             undefined, | ||||
|         ); | ||||
| 
 | ||||
|         this.acceptedTypes = this.data?.configs.filetypeslist; | ||||
|         this.maxSize = this.data?.configs.maxsubmissionsizebytes | ||||
|             ? parseInt(this.data?.configs.maxsubmissionsizebytes, 10) | ||||
|             : undefined; | ||||
|         this.maxSubmissions = this.data?.configs.maxfilesubmissions | ||||
|             ? parseInt(this.data?.configs.maxfilesubmissions, 10) | ||||
|             : undefined; | ||||
| 
 | ||||
|         try { | ||||
|             if (filesData && filesData.plugindata && filesData.plugindata.files_filemanager) { | ||||
|                 const offlineDataFiles = <CoreFileUploaderStoreFilesResult>filesData.plugindata.files_filemanager; | ||||
|                 // It has offline data.
 | ||||
|                 let offlineFiles: FileEntry[] = []; | ||||
|                 if (offlineDataFiles.offline) { | ||||
|                     offlineFiles = <FileEntry[]>await CoreUtils.instance.ignoreErrors( | ||||
|                         AddonModAssignHelper.instance.getStoredSubmissionFiles( | ||||
|                             this.assign.id, | ||||
|                             AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, | ||||
|                         ), | ||||
|                         [], | ||||
|                     ); | ||||
|                 } | ||||
| 
 | ||||
|                 this.files = offlineDataFiles.online || []; | ||||
|                 this.files = this.files.concat(offlineFiles); | ||||
|             } else { | ||||
|                 // No offline data, get the online files.
 | ||||
|                 this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin); | ||||
|             } | ||||
|         } finally  { | ||||
|             CoreFileSession.instance.setFiles(this.component, this.assign.id, this.files); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										45
									
								
								src/addons/mod/assign/submission/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addons/mod/assign/submission/file/file.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { AddonModAssignSubmissionFileHandler } from './services/handler'; | ||||
| import { AddonModAssignSubmissionFileComponent } from './component/file'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModAssignSubmissionFileComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionFileHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModAssignSubmissionFileComponent, | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonModAssignSubmissionFileComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignSubmissionFileModule {} | ||||
							
								
								
									
										3
									
								
								src/addons/mod/assign/submission/file/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/mod/assign/submission/file/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| { | ||||
|     "pluginname": "File submissions" | ||||
| } | ||||
							
								
								
									
										388
									
								
								src/addons/mod/assign/submission/file/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								src/addons/mod/assign/submission/file/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,388 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssignPlugin, | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssign, | ||||
| } from '@addons/mod/assign/services/assign'; | ||||
| import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper'; | ||||
| import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } from '@addons/mod/assign/services/assign-offline'; | ||||
| import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; | ||||
| import { CoreFileHelper } from '@services/file-helper'; | ||||
| import { CoreFileSession } from '@services/file-session'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { AddonModAssignSubmissionFileComponent } from '../component/file'; | ||||
| import { FileEntry } from '@ionic-native/file/ngx'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for file submission plugin. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' }) | ||||
| export class AddonModAssignSubmissionFileHandlerService implements AddonModAssignSubmissionHandler { | ||||
| 
 | ||||
|     static readonly FOLDER_NAME = 'submission_file'; | ||||
| 
 | ||||
|     name = 'AddonModAssignSubmissionFileHandler'; | ||||
|     type = 'file'; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the | ||||
|      * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit | ||||
|      * unfiltered data. | ||||
|      * | ||||
|      * @return Boolean or promise resolved with boolean: whether it can be edited in offline. | ||||
|      */ | ||||
|     canEditOffline(): boolean { | ||||
|         // This plugin doesn't use Moodle filters, it can be edited in offline.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a plugin has no data. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return Whether the plugin is empty. | ||||
|      */ | ||||
|     isEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean { | ||||
|         const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
| 
 | ||||
|         return files.length === 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Should clear temporary data for a cancelled submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      */ | ||||
|     clearTmpData(assign: AddonModAssignAssign): void { | ||||
|         const files = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id); | ||||
| 
 | ||||
|         // Clear the files in session for this assign.
 | ||||
|         CoreFileSession.instance.clearFiles(AddonModAssignProvider.COMPONENT, assign.id); | ||||
| 
 | ||||
|         // Now delete the local files from the tmp folder.
 | ||||
|         CoreFileUploader.instance.clearTmpFiles(files); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This function will be called when the user wants to create a new submission based on the previous one. | ||||
|      * It should add to pluginData the data to send to server based in the data in plugin (previous attempt). | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     async copySubmissionData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         pluginData: AddonModAssignSubmissionFilePluginData, | ||||
|     ): Promise<void> { | ||||
|         // We need to re-upload all the existing files.
 | ||||
|         const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
| 
 | ||||
|         // Get the itemId.
 | ||||
|         pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadFiles(assign.id, files); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the plugin data, either in read or in edit mode. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @return The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent(): Type<unknown> { | ||||
|         return AddonModAssignSubmissionFileComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete any stored data for the plugin and submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param offlineData Offline data stored. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     async deleteOfflineData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         offlineData: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         await CoreUtils.instance.ignoreErrors( | ||||
|             AddonModAssignHelper.instance.deleteStoredSubmissionFiles( | ||||
|                 assign.id, | ||||
|                 AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, | ||||
|                 submission.userid, | ||||
|                 siteId, | ||||
|             ), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): CoreWSExternalFile[] { | ||||
|         return AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to copy a previous submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The size (or promise resolved with size). | ||||
|      */ | ||||
|     async getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise<number> { | ||||
|         const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
| 
 | ||||
|         return CoreFileHelper.instance.getTotalFilesSize(files); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to add or edit a submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The size (or promise resolved with size). | ||||
|      */ | ||||
|     async getSizeForEdit( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): Promise<number> { | ||||
|         // Check if there's any change.
 | ||||
|         if (this.hasDataChanged(assign, submission, plugin)) { | ||||
|             const files = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id); | ||||
| 
 | ||||
|             return CoreFileHelper.instance.getTotalFilesSize(files); | ||||
|         } else { | ||||
|             // Nothing has changed, we won't upload any file.
 | ||||
|             return 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the submission data has changed for this plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return Boolean (or promise resolved with boolean): whether the data has changed. | ||||
|      */ | ||||
|     async hasDataChanged( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): Promise<boolean> { | ||||
|         const offlineData = await CoreUtils.instance.ignoreErrors( | ||||
|             // Check if there's any offline data.
 | ||||
|             AddonModAssignOffline.instance.getSubmission(assign.id, submission.userid), | ||||
|             undefined, | ||||
|         ); | ||||
| 
 | ||||
|         let numFiles: number; | ||||
|         if (offlineData && offlineData.plugindata && offlineData.plugindata.files_filemanager) { | ||||
|             const offlineDataFiles = <CoreFileUploaderStoreFilesResult>offlineData.plugindata.files_filemanager; | ||||
|             // Has offline data, return the number of files.
 | ||||
|             numFiles = offlineDataFiles.offline + offlineDataFiles.online.length; | ||||
|         } else { | ||||
|             // No offline data, return the number of online files.
 | ||||
|             const pluginFiles = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
| 
 | ||||
|             numFiles = pluginFiles && pluginFiles.length; | ||||
|         } | ||||
| 
 | ||||
|         const currentFiles = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id); | ||||
| 
 | ||||
|         if (currentFiles.length != numFiles) { | ||||
|             // Number of files has changed.
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         const files = await this.getSubmissionFilesToSync(assign, submission, offlineData); | ||||
| 
 | ||||
|         // Check if there is any local file added and list has changed.
 | ||||
|         return CoreFileUploader.instance.areFileListDifferent(currentFiles, files); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled for edit on a site level. | ||||
|      * | ||||
|      * @return Whether or not the handler is enabled for edit on a site level. | ||||
|      */ | ||||
|     isEnabledForEdit(): boolean { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the input data. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param offline Whether the user is editing in offline. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     async prepareSubmissionData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: AddonModAssignSubmissionFileData, | ||||
|         pluginData: AddonModAssignSubmissionFilePluginData, | ||||
|         offline?: boolean, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const changed = await this.hasDataChanged(assign, submission, plugin); | ||||
|         if (!changed) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Data has changed, we need to upload new files and re-upload all the existing files.
 | ||||
|         const currentFiles = CoreFileSession.instance.getFiles(AddonModAssignProvider.COMPONENT, assign.id); | ||||
|         const error = CoreUtils.instance.hasRepeatedFilenames(currentFiles); | ||||
| 
 | ||||
|         if (error) { | ||||
|             throw error; | ||||
|         } | ||||
| 
 | ||||
|         pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadOrStoreFiles( | ||||
|             assign.id, | ||||
|             AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, | ||||
|             currentFiles, | ||||
|             offline, | ||||
|             userId, | ||||
|             siteId, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the offline data stored. | ||||
|      * This will be used when performing a synchronization. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param offlineData Offline data stored. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     async prepareSyncData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         offlineData: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         pluginData: AddonModAssignSubmissionFilePluginData, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const files = await this.getSubmissionFilesToSync(assign, submission, offlineData, siteId); | ||||
| 
 | ||||
|         if (files.length == 0) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         pluginData.files_filemanager = await AddonModAssignHelper.instance.uploadFiles(assign.id, files, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the file list to be synced. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param offlineData Offline data stored. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return File entries when is all resolved. | ||||
|      */ | ||||
|     protected async getSubmissionFilesToSync( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         offlineData?: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         siteId?: string, | ||||
|     ): Promise<(FileEntry | CoreWSExternalFile)[]> { | ||||
|         const filesData = <CoreFileUploaderStoreFilesResult>offlineData?.plugindata.files_filemanager; | ||||
|         if (!filesData) { | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         // Has some data to sync.
 | ||||
|         let files: (FileEntry | CoreWSExternalFile)[] = filesData.online || []; | ||||
| 
 | ||||
|         if (filesData.offline) { | ||||
|             // Has offline files, get them and add them to the list.
 | ||||
|             const storedFiles = <FileEntry[]> await CoreUtils.instance.ignoreErrors( | ||||
|                 AddonModAssignHelper.instance.getStoredSubmissionFiles( | ||||
|                     assign.id, | ||||
|                     AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, | ||||
|                     submission.userid, | ||||
|                     siteId, | ||||
|                 ), | ||||
|                 [], | ||||
|             ); | ||||
|             files = files.concat(storedFiles); | ||||
|         } | ||||
| 
 | ||||
|         return files; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignSubmissionFileHandler = makeSingleton(AddonModAssignSubmissionFileHandlerService); | ||||
| 
 | ||||
| // Define if ever used.
 | ||||
| export type AddonModAssignSubmissionFileData = Record<string, unknown>; | ||||
| 
 | ||||
| export type AddonModAssignSubmissionFilePluginData = { | ||||
|     // The id of a draft area containing files for this submission. Or the offline file results.
 | ||||
|     files_filemanager: number | CoreFileUploaderStoreFilesResult; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
| }; | ||||
| @ -0,0 +1,34 @@ | ||||
| <!-- Read only --> | ||||
| <ion-item class="ion-text-wrap" *ngIf="!edit && text"> | ||||
|     <ion-label> | ||||
|         <h2>{{ plugin.name }}</h2> | ||||
|         <p *ngIf="words">{{ 'addon.mod_assign.numwords' | translate: {'$a': words} }}</p> | ||||
|         <p> | ||||
|             <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" | ||||
|                 [fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid" | ||||
|                 [courseId]="assign.course"> | ||||
|             </core-format-text> | ||||
|         </p> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
| 
 | ||||
| <!-- Edit --> | ||||
| <div *ngIf="edit && loaded"> | ||||
|     <ion-item-divider class="ion-text-wrap" sticky="true"> | ||||
|         <ion-label><h2>{{ plugin.name }}</h2></ion-label> | ||||
|     </ion-item-divider> | ||||
|     <ion-item class="ion-text-wrap" *ngIf="wordLimitEnabled && words >= 0"> | ||||
|         <ion-label> | ||||
|             <h2>{{ 'addon.mod_assign.wordlimit' | translate }}</h2> | ||||
|             <p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + wordLimit} }}</p> | ||||
|         </ion-label> | ||||
|     </ion-item> | ||||
|     <ion-item class="ion-text-wrap"> | ||||
|         <ion-label></ion-label> | ||||
|         <core-rich-text-editor [control]="control" [placeholder]="plugin.name" | ||||
|             name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" | ||||
|             [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" | ||||
|             elementId="onlinetext_editor" [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}"> | ||||
|         </core-rich-text-editor> | ||||
|     </ion-item> | ||||
| </div> | ||||
| @ -0,0 +1,130 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AddonModAssignSubmissionPluginComponent } from '@addons/mod/assign/components/submission-plugin/submission-plugin'; | ||||
| import { AddonModAssignProvider, AddonModAssign } from '@addons/mod/assign/services/assign'; | ||||
| import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offline'; | ||||
| import { Component, OnInit, ElementRef } from '@angular/core'; | ||||
| import { FormBuilder, FormControl } from '@angular/forms'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { AddonModAssignSubmissionOnlineTextPluginData } from '../services/handler'; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Component to render an onlinetext submission plugin. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-assign-submission-online-text', | ||||
|     templateUrl: 'addon-mod-assign-submission-onlinetext.html', | ||||
| }) | ||||
| export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignSubmissionPluginComponent implements OnInit { | ||||
| 
 | ||||
|     control?: FormControl; | ||||
|     words = 0; | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     text = ''; | ||||
|     loaded = false; | ||||
|     wordLimitEnabled = false; | ||||
|     currentUserId: number; | ||||
|     wordLimit = 0; | ||||
| 
 | ||||
|     protected wordCountTimeout?: number; | ||||
|     protected element: HTMLElement; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected fb: FormBuilder, | ||||
|         element: ElementRef, | ||||
|     ) { | ||||
|         super(); | ||||
|         this.element = element.nativeElement; | ||||
|         this.currentUserId = CoreSites.instance.getCurrentSiteUserId(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         // Get the text. Check if we have anything offline.
 | ||||
|         const offlineData = await CoreUtils.instance.ignoreErrors( | ||||
|             AddonModAssignOffline.instance.getSubmission(this.assign.id), | ||||
|             undefined, | ||||
|         ); | ||||
| 
 | ||||
|         this.wordLimitEnabled = !!parseInt(this.data?.configs.wordlimitenabled || '0', 10); | ||||
|         this.wordLimit = parseInt(this.data?.configs.wordlimit || '0'); | ||||
| 
 | ||||
|         try { | ||||
|             if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) { | ||||
|                 this.text = (<AddonModAssignSubmissionOnlineTextPluginData>offlineData.plugindata).onlinetext_editor.text; | ||||
|             } else { | ||||
|                 // No offline data found, return online text.
 | ||||
|                 this.text = AddonModAssign.instance.getSubmissionPluginText(this.plugin); | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             // Set the text.
 | ||||
|             if (!this.edit) { | ||||
|                 // Not editing, see full text when clicked.
 | ||||
|                 this.element.addEventListener('click', (e) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
| 
 | ||||
|                     if (this.text) { | ||||
|                         // Open a new state with the interpolated contents.
 | ||||
|                         CoreTextUtils.instance.viewText(this.plugin.name, this.text, { | ||||
|                             component: this.component, | ||||
|                             componentId: this.assign.cmid, | ||||
|                             filter: true, | ||||
|                             contextLevel: 'module', | ||||
|                             instanceId: this.assign.cmid, | ||||
|                             courseId: this.assign.course, | ||||
|                         }); | ||||
|                     } | ||||
|                 }); | ||||
|             } else { | ||||
|                 // Create and add the control.
 | ||||
|                 this.control = this.fb.control(this.text); | ||||
|             } | ||||
| 
 | ||||
|             // Calculate initial words.
 | ||||
|             if (this.wordLimitEnabled) { | ||||
|                 this.words = CoreTextUtils.instance.countWords(this.text); | ||||
|             } | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Text changed. | ||||
|      * | ||||
|      * @param text The new text. | ||||
|      */ | ||||
|     onChange(text: string): void { | ||||
|         // Count words if needed.
 | ||||
|         if (this.wordLimitEnabled) { | ||||
|             // Cancel previous wait.
 | ||||
|             clearTimeout(this.wordCountTimeout); | ||||
| 
 | ||||
|             // Wait before calculating, if the user keeps inputing we won't calculate.
 | ||||
|             // This is to prevent slowing down devices, this calculation can be slow if the text is long.
 | ||||
|             this.wordCountTimeout = window.setTimeout(() => { | ||||
|                 this.words = CoreTextUtils.instance.countWords(text); | ||||
|             }, 1500); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										4
									
								
								src/addons/mod/assign/submission/onlinetext/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/addons/mod/assign/submission/onlinetext/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| { | ||||
|     "pluginname": "Online text submissions", | ||||
|     "wordlimitexceeded": "The word limit for this assignment is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please review your submission and try again." | ||||
| } | ||||
| @ -0,0 +1,47 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { AddonModAssignSubmissionOnlineTextHandler } from './services/handler'; | ||||
| import { AddonModAssignSubmissionOnlineTextComponent } from './component/onlinetext'; | ||||
| import { CoreEditorComponentsModule } from '@features/editor/components/components.module'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModAssignSubmissionOnlineTextComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|         CoreEditorComponentsModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonModAssignSubmissionDelegate.instance.registerHandler(AddonModAssignSubmissionOnlineTextHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModAssignSubmissionOnlineTextComponent, | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonModAssignSubmissionOnlineTextComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignSubmissionOnlineTextModule {} | ||||
							
								
								
									
										323
									
								
								src/addons/mod/assign/submission/onlinetext/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								src/addons/mod/assign/submission/onlinetext/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,323 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssignPlugin, | ||||
|     AddonModAssign, | ||||
| } from '@addons/mod/assign/services/assign'; | ||||
| import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper'; | ||||
| import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } from '@addons/mod/assign/services/assign-offline'; | ||||
| import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreFileHelper } from '@services/file-helper'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { AddonModAssignSubmissionOnlineTextComponent } from '../component/onlinetext'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for online text submission plugin. | ||||
|  */ | ||||
| @Injectable( { providedIn: 'root' }) | ||||
| export class AddonModAssignSubmissionOnlineTextHandlerService implements AddonModAssignSubmissionHandler { | ||||
| 
 | ||||
|     name = 'AddonModAssignSubmissionOnlineTextHandler'; | ||||
|     type = 'onlinetext'; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the | ||||
|      * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit | ||||
|      * unfiltered data. | ||||
|      * | ||||
|      * @return Boolean or promise resolved with boolean: whether it can be edited in offline. | ||||
|      */ | ||||
|     canEditOffline(): boolean { | ||||
|         // This plugin uses Moodle filters, it cannot be edited in offline.
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a plugin has no data. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return Whether the plugin is empty. | ||||
|      */ | ||||
|     isEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean { | ||||
|         const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true); | ||||
| 
 | ||||
|         // If the text is empty, we can ignore files because they won't be visible anyways.
 | ||||
|         return text.trim().length === 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This function will be called when the user wants to create a new submission based on the previous one. | ||||
|      * It should add to pluginData the data to send to server based in the data in plugin (previous attempt). | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     async copySubmissionData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         pluginData: AddonModAssignSubmissionOnlineTextPluginData, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true); | ||||
|         const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
|         let itemId = 0; | ||||
| 
 | ||||
|         if (files.length) { | ||||
|             // Re-upload the files.
 | ||||
|             itemId = await AddonModAssignHelper.instance.uploadFiles(assign.id, files, siteId); | ||||
|         } | ||||
| 
 | ||||
|         pluginData.onlinetext_editor = { | ||||
|             text: text, | ||||
|             format: 1, | ||||
|             itemid: itemId, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the plugin data, either in read or in edit mode. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @return The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent(): Type<unknown> { | ||||
|         return AddonModAssignSubmissionOnlineTextComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get files used by this plugin. | ||||
|      * The files returned by this function will be prefetched when the user prefetches the assign. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The files (or promise resolved with the files). | ||||
|      */ | ||||
|     getPluginFiles( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): CoreWSExternalFile[] { | ||||
|         return AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to copy a previous submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param plugin The plugin object. | ||||
|      * @return The size (or promise resolved with size). | ||||
|      */ | ||||
|     async getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise<number> { | ||||
|         const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true); | ||||
|         const files = AddonModAssign.instance.getSubmissionPluginAttachments(plugin); | ||||
| 
 | ||||
|         const filesSize = await CoreFileHelper.instance.getTotalFilesSize(files); | ||||
| 
 | ||||
|         return text.length + filesSize; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the size of data (in bytes) this plugin will send to add or edit a submission. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @return The size (or promise resolved with size). | ||||
|      */ | ||||
|     getSizeForEdit( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|     ): number { | ||||
|         const text = AddonModAssign.instance.getSubmissionPluginText(plugin, true); | ||||
| 
 | ||||
|         return text.length; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the text to submit. | ||||
|      * | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @return Text to submit. | ||||
|      */ | ||||
|     protected getTextToSubmit(plugin: AddonModAssignPlugin, inputData: AddonModAssignSubmissionOnlineTextData): string { | ||||
|         const text = inputData.onlinetext_editor_text; | ||||
|         const files = plugin.fileareas && plugin.fileareas[0] && plugin.fileareas[0].files || []; | ||||
| 
 | ||||
|         return CoreTextUtils.instance.restorePluginfileUrls(text, files || []); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the submission data has changed for this plugin. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @return Boolean (or promise resolved with boolean): whether the data has changed. | ||||
|      */ | ||||
|     async hasDataChanged( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: AddonModAssignSubmissionOnlineTextData, | ||||
|     ): Promise<boolean> { | ||||
| 
 | ||||
|         // Get the original text from plugin or offline.
 | ||||
|         const offlineData = | ||||
|             await CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getSubmission(assign.id, submission.userid)); | ||||
| 
 | ||||
|         let initialText = ''; | ||||
|         if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) { | ||||
|             initialText = (<AddonModAssignSubmissionOnlineTextPluginData>offlineData.plugindata).onlinetext_editor.text; | ||||
|         } else { | ||||
|             // No offline data found, get text from plugin.
 | ||||
|             initialText = plugin.editorfields && plugin.editorfields[0] ? plugin.editorfields[0].text : ''; | ||||
|         } | ||||
| 
 | ||||
|         // Check if text has changed.
 | ||||
|         return initialText != this.getTextToSubmit(plugin, inputData); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled for edit on a site level. | ||||
|      * | ||||
|      * @return Whether or not the handler is enabled for edit on a site level. | ||||
|      */ | ||||
|     isEnabledForEdit(): boolean { | ||||
|         // There's a bug in Moodle 3.1.0 that doesn't allow submitting HTML, so we'll disable this plugin in that case.
 | ||||
|         // Bug was fixed in 3.1.1 minor release and in 3.2.
 | ||||
|         const currentSite = CoreSites.instance.getCurrentSite(); | ||||
| 
 | ||||
|         return !!currentSite?.isVersionGreaterEqualThan('3.1.1') || !!currentSite?.checkIfAppUsesLocalMobile(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the input data. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param inputData Data entered by the user for the submission. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param offline Whether the user is editing in offline. | ||||
|      * @param userId User ID. If not defined, site's current user. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     prepareSubmissionData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: AddonModAssignSubmissionOnlineTextData, | ||||
|         pluginData: AddonModAssignSubmissionOnlineTextPluginData, | ||||
|     ): void | Promise<void> { | ||||
| 
 | ||||
|         let text = this.getTextToSubmit(plugin, inputData); | ||||
| 
 | ||||
|         // Check word limit.
 | ||||
|         const configs = AddonModAssignHelper.instance.getPluginConfig(assign, 'assignsubmission', plugin.type); | ||||
|         if (parseInt(configs.wordlimitenabled, 10)) { | ||||
|             const words = CoreTextUtils.instance.countWords(text); | ||||
|             const wordlimit = parseInt(configs.wordlimit, 10); | ||||
|             if (words > wordlimit) { | ||||
|                 const params = { $a: { count: words, limit: wordlimit } }; | ||||
|                 const message = Translate.instance.instant('addon.mod_assign_submission_onlinetext.wordlimitexceeded', params); | ||||
| 
 | ||||
|                 throw new CoreError(message); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Add some HTML to the text if needed.
 | ||||
|         text = CoreTextUtils.instance.formatHtmlLines(text); | ||||
| 
 | ||||
|         pluginData.onlinetext_editor = { | ||||
|             text: text, | ||||
|             format: 1, | ||||
|             itemid: 0, // Can't add new files yet, so we use a fake itemid.
 | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare and add to pluginData the data to send to the server based on the offline data stored. | ||||
|      * This will be used when performing a synchronization. | ||||
|      * | ||||
|      * @param assign The assignment. | ||||
|      * @param submission The submission. | ||||
|      * @param plugin The plugin object. | ||||
|      * @param offlineData Offline data stored. | ||||
|      * @param pluginData Object where to store the data to send. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return If the function is async, it should return a Promise resolved when done. | ||||
|      */ | ||||
|     prepareSyncData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         offlineData: AddonModAssignSubmissionsDBRecordFormatted, | ||||
|         pluginData: AddonModAssignSubmissionOnlineTextPluginData, | ||||
|     ): void | Promise<void> { | ||||
| 
 | ||||
|         const offlinePluginData = <AddonModAssignSubmissionOnlineTextPluginData>(offlineData && offlineData.plugindata); | ||||
|         const textData = offlinePluginData.onlinetext_editor; | ||||
|         if (textData) { | ||||
|             // Has some data to sync.
 | ||||
|             pluginData.onlinetext_editor = textData; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignSubmissionOnlineTextHandler = makeSingleton(AddonModAssignSubmissionOnlineTextHandlerService); | ||||
| 
 | ||||
| export type AddonModAssignSubmissionOnlineTextData = { | ||||
|     // The text for this submission.
 | ||||
|     onlinetext_editor_text: string; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
| }; | ||||
| 
 | ||||
| export type AddonModAssignSubmissionOnlineTextPluginData = { | ||||
|     // Editor structure.
 | ||||
|     onlinetext_editor: { // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|         text: string; // The text for this submission.
 | ||||
|         format: number; // The format for this submission.
 | ||||
|         itemid: number; // The draft area id for files attached to the submission.
 | ||||
|     }; | ||||
| }; | ||||
							
								
								
									
										27
									
								
								src/addons/mod/assign/submission/submission.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/addons/mod/assign/submission/submission.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { AddonModAssignSubmissionCommentsModule } from './comments/comments.module'; | ||||
| import { AddonModAssignSubmissionFileModule } from './file/file.module'; | ||||
| import { AddonModAssignSubmissionOnlineTextModule } from './onlinetext/onlinetext.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         AddonModAssignSubmissionCommentsModule, | ||||
|         AddonModAssignSubmissionFileModule, | ||||
|         AddonModAssignSubmissionOnlineTextModule, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignSubmissionModule { } | ||||
| @ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: ':courseId/:cmdId', | ||||
|         path: ':courseId/:cmId', | ||||
|         loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModBookIndexPageModule), | ||||
|     }, | ||||
| ]; | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" | ||||
|             [iconAction]="'far-newspaper'" (action)="gotoBlog()"> | ||||
|             iconAction="far-newspaper" (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|  | ||||
| @ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: ':courseId/:cmdId', | ||||
|         path: ':courseId/:cmId', | ||||
|         loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModLessonIndexPageModule), | ||||
|     }, | ||||
|     { | ||||
|  | ||||
| @ -257,7 +257,7 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref | ||||
|      */ | ||||
|     protected async prefetchLesson(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean): Promise<void> { | ||||
|         const siteId = CoreSites.instance.getCurrentSiteId(); | ||||
|         courseId = courseId || module.course || 1; | ||||
|         courseId = courseId || module.course || CoreSites.instance.getCurrentSiteHomeId(); | ||||
| 
 | ||||
|         const commonOptions = { | ||||
|             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreSyncBlockedError } from '@classes/base-sync'; | ||||
| import { CoreNetworkError } from '@classes/errors/network-error'; | ||||
| import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| @ -122,7 +122,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     syncAllLessons(siteId?: string, force?: boolean): Promise<void> { | ||||
|     syncAllLessons(siteId?: string, force = false): Promise<void> { | ||||
|         return this.syncOnSites('all lessons', this.syncAllLessonsFunc.bind(this, !!force), siteId); | ||||
|     } | ||||
| 
 | ||||
| @ -163,7 +163,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid | ||||
|      */ | ||||
|     async syncLessonIfNeeded( | ||||
|         lessonId: number, | ||||
|         askPassword?: boolean, | ||||
|         askPassword = false, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModLessonSyncResult | undefined> { | ||||
|         const needed = await this.isSyncNeeded(lessonId, siteId); | ||||
| @ -184,8 +184,8 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid | ||||
|      */ | ||||
|     async syncLesson( | ||||
|         lessonId: number, | ||||
|         askPassword?: boolean, | ||||
|         ignoreBlock?: boolean, | ||||
|         askPassword = false, | ||||
|         ignoreBlock = false, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModLessonSyncResult> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| @ -201,7 +201,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid | ||||
|         if (!ignoreBlock && CoreSync.instance.isBlocked(AddonModLessonProvider.COMPONENT, lessonId, siteId)) { | ||||
|             this.logger.debug('Cannot sync lesson ' + lessonId + ' because it is blocked.'); | ||||
| 
 | ||||
|             throw new CoreError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate })); | ||||
|             throw new CoreSyncBlockedError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate })); | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Try to sync lesson ' + lessonId + ' in site ' + siteId); | ||||
| @ -222,8 +222,8 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid | ||||
|      */ | ||||
|     protected async performSyncLesson( | ||||
|         lessonId: number, | ||||
|         askPassword?: boolean, | ||||
|         ignoreBlock?: boolean, | ||||
|         askPassword = false, | ||||
|         ignoreBlock = false, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModLessonSyncResult> { | ||||
|         // Sync offline logs.
 | ||||
| @ -270,7 +270,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid | ||||
|     protected async syncAttempts( | ||||
|         lessonId: number, | ||||
|         result: AddonModLessonSyncResult, | ||||
|         askPassword?: boolean, | ||||
|         askPassword = false, | ||||
|         siteId?: string, | ||||
|     ): Promise<AddonModLessonGetPasswordResult | undefined> { | ||||
|         let attempts = await AddonModLessonOffline.instance.getLessonAttempts(lessonId, siteId); | ||||
| @ -408,8 +408,8 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid | ||||
|         lessonId: number, | ||||
|         result: AddonModLessonSyncResult, | ||||
|         passwordData?: AddonModLessonGetPasswordResult, | ||||
|         askPassword?: boolean, | ||||
|         ignoreBlock?: boolean, | ||||
|         askPassword = false, | ||||
|         ignoreBlock = false, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
|         // Attempts sent or there was none. If there is a finished retake, send it.
 | ||||
|  | ||||
| @ -14,6 +14,7 @@ | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| 
 | ||||
| import { AddonModAssignModule } from './assign/assign.module'; | ||||
| import { AddonModBookModule } from './book/book.module'; | ||||
| import { AddonModLessonModule } from './lesson/lesson.module'; | ||||
| import { AddonModPageModule } from './page/page.module'; | ||||
| @ -21,6 +22,7 @@ import { AddonModPageModule } from './page/page.module'; | ||||
| @NgModule({ | ||||
|     declarations: [], | ||||
|     imports: [ | ||||
|         AddonModAssignModule, | ||||
|         AddonModBookModule, | ||||
|         AddonModLessonModule, | ||||
|         AddonModPageModule, | ||||
|  | ||||
| @ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: ':courseId/:cmdId', | ||||
|         path: ':courseId/:cmId', | ||||
|         loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModPageIndexPageModule), | ||||
|     }, | ||||
| ]; | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/grades/agg_mean.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/img/grades/agg_mean.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 341 B | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/grades/agg_sum.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/img/grades/agg_sum.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 318 B | 
| @ -231,7 +231,7 @@ export class CoreSyncBaseProvider<T = void> { | ||||
|      * @param time Time to set. If not defined, current time. | ||||
|      * @return Promise resolved when the time is set. | ||||
|      */ | ||||
|     async setSyncTime(id: string, siteId?: string, time?: number): Promise<void> { | ||||
|     async setSyncTime(id: string | number, siteId?: string, time?: number): Promise<void> { | ||||
|         time = typeof time != 'undefined' ? time : Date.now(); | ||||
| 
 | ||||
|         await CoreSync.instance.insertOrUpdateSyncRecord(this.component, id, { time: time }, siteId); | ||||
| @ -245,7 +245,7 @@ export class CoreSyncBaseProvider<T = void> { | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async setSyncWarnings(id: string, warnings: string[], siteId?: string): Promise<void> { | ||||
|     async setSyncWarnings(id: string | number, warnings: string[], siteId?: string): Promise<void> { | ||||
|         const warningsText = JSON.stringify(warnings || []); | ||||
| 
 | ||||
|         await CoreSync.instance.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId); | ||||
|  | ||||
| @ -42,7 +42,7 @@ export class CoreIonLoadingElement { | ||||
|      * Present the loading. | ||||
|      */ | ||||
|     async present(): Promise<void> { | ||||
|         // Wait a bit before presenting the modal, to prevent it being displayed if dissmiss is called fast.
 | ||||
|         // Wait a bit before presenting the modal, to prevent it being displayed if dismiss is called fast.
 | ||||
|         await CoreUtils.instance.wait(40); | ||||
| 
 | ||||
|         if (!this.isDismissed) { | ||||
|  | ||||
| @ -55,8 +55,12 @@ export abstract class CorePageItemsListManager<Item> { | ||||
| 
 | ||||
|     /** | ||||
|      * Process page started operations. | ||||
|      * | ||||
|      * @param splitView Split view component. | ||||
|      */ | ||||
|     async start(): Promise<void> { | ||||
|     async start(splitView: CoreSplitViewComponent): Promise<void> { | ||||
|         this.watchSplitViewOutlet(splitView); | ||||
| 
 | ||||
|         // Calculate current selected item.
 | ||||
|         const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent }); | ||||
|         if (route !== null && route.firstChild) { | ||||
|  | ||||
| @ -71,6 +71,7 @@ export class CoreFaIconDirective implements OnChanges { | ||||
|         if (library != 'ionic') { | ||||
|             const src = `assets/fonts/font-awesome/${library}/${iconName}.svg`; | ||||
|             this.element.setAttribute('src', src); | ||||
|             this.element.classList.add('faicon'); | ||||
| 
 | ||||
|             if (CoreConstants.BUILD.isDevelopment || CoreConstants.BUILD.isTesting) { | ||||
|                 try { | ||||
|  | ||||
| @ -20,7 +20,7 @@ import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler'; | ||||
| /** | ||||
|  * Base class to create activity sync providers. It provides some common functions. | ||||
|  */ | ||||
| export class CoreCourseActivitySyncBaseProvider<T> extends CoreSyncBaseProvider<T> { | ||||
| export class CoreCourseActivitySyncBaseProvider<T = void> extends CoreSyncBaseProvider<T> { | ||||
| 
 | ||||
|     /** | ||||
|      * Conveniece function to prefetch data after an update. | ||||
|  | ||||
| @ -522,7 +522,14 @@ export class CoreCourseProvider { | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the module's grade info. | ||||
|      */ | ||||
|     async getModuleBasicGradeInfo(moduleId: number, siteId?: string): Promise<CoreCourseModuleGradeInfo | false> { | ||||
|     async getModuleBasicGradeInfo(moduleId: number, siteId?: string): Promise<CoreCourseModuleGradeInfo | undefined> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         if (!site || !site.isVersionGreaterEqualThan('3.2')) { | ||||
|             // On 3.1 won't get grading info and will return undefined. See check bellow.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const info = await this.getModuleBasicInfo(moduleId, siteId); | ||||
| 
 | ||||
|         const grade: CoreCourseModuleGradeInfo = { | ||||
| @ -539,10 +546,11 @@ export class CoreCourseProvider { | ||||
|             typeof grade.advancedgrading != 'undefined' || | ||||
|             typeof grade.outcomes != 'undefined' | ||||
|         ) { | ||||
|             // On 3.1 won't get grading info and will return undefined.
 | ||||
|             return grade; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -1461,22 +1469,32 @@ export type CoreCourseModuleContentFile = { | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Course module basic info type. | ||||
|  * Course module basic info type. 3.2 onwards. | ||||
|  */ | ||||
| export type CoreCourseModuleGradeInfo = { | ||||
|     grade?: number; // Grade (max value or scale id).
 | ||||
|     scale?: string; // Scale items (if used).
 | ||||
|     gradepass?: string; // Grade to pass (float).
 | ||||
|     gradecat?: number; // Grade category.
 | ||||
|     advancedgrading?: { // Advanced grading settings.
 | ||||
|         area: string; // Gradable area name.
 | ||||
|         method: string; // Grading method.
 | ||||
|     }[]; | ||||
|     outcomes?: { // Outcomes information.
 | ||||
|         id: string; // Outcome id.
 | ||||
|         name: string; // Outcome full name.
 | ||||
|         scale: string; // Scale items.
 | ||||
|     }[]; | ||||
|     advancedgrading?: CoreCourseModuleAdvancedGradingSetting[]; // Advanced grading settings.
 | ||||
|     outcomes?: CoreCourseModuleGradeOutcome[]; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Advanced grading settings. | ||||
|  */ | ||||
| export type CoreCourseModuleAdvancedGradingSetting = { | ||||
|     area: string; // Gradable area name.
 | ||||
|     method: string; // Grading method.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Grade outcome information. | ||||
|  */ | ||||
| export type CoreCourseModuleGradeOutcome = { | ||||
|     id: string; // Outcome id.
 | ||||
|     name: string; // Outcome full name.
 | ||||
|     scale: string; // Scale items.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| @import "~theme/globals"; | ||||
| 
 | ||||
| :host { | ||||
|     .course-icon { | ||||
|         color: white; | ||||
| @ -10,35 +12,10 @@ | ||||
|         transition: all 50ms ease-in-out; | ||||
|     } | ||||
| 
 | ||||
|     ion-icon[course-color="0"] { | ||||
|         color: var(--core-course-color-0); | ||||
|     } | ||||
|     ion-icon[course-color="1"] { | ||||
|         color: var(--core-course-color-1); | ||||
|     } | ||||
|     ion-icon[course-color="2"] { | ||||
|         color: var(--core-course-color-2); | ||||
|     } | ||||
|     ion-icon[course-color="3"] { | ||||
|         color: var(--core-course-color-3); | ||||
|     } | ||||
|     ion-icon[course-color="4"] { | ||||
|         color: var(--core-course-color-4); | ||||
|     } | ||||
|     ion-icon[course-color="5"] { | ||||
|         color: var(--core-course-color-5); | ||||
|     } | ||||
|     ion-icon[course-color="6"] { | ||||
|         color: var(--core-course-color-6); | ||||
|     } | ||||
|     ion-icon[course-color="7"] { | ||||
|         color: var(--core-course-color-7); | ||||
|     } | ||||
|     ion-icon[course-color="8"] { | ||||
|         color: var(--core-course-color-8); | ||||
|     } | ||||
|     ion-icon[course-color="9"] { | ||||
|         color: var(--core-course-color-9); | ||||
|     @for $i from 0 to length($core-course-image-background) { | ||||
|         ion-icon[course-color="#{$i}"] { | ||||
|             color: nth($core-course-image-background, $i + 1); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ion-avatar { | ||||
|  | ||||
| @ -7,35 +7,10 @@ | ||||
|         align-self: stretch; | ||||
|         height: calc(100% - 20px); | ||||
| 
 | ||||
|         &[course-color="0"] .core-course-thumb { | ||||
|             background: var(--core-course-color-0); | ||||
|         } | ||||
|         &[course-color="1"] .core-course-thumb { | ||||
|             background: var(--core-course-color-1); | ||||
|         } | ||||
|         &[course-color="2"] .core-course-thumb { | ||||
|             background: var(--core-course-color-2); | ||||
|         } | ||||
|         &[course-color="3"] .core-course-thumb { | ||||
|             background: var(--core-course-color-3); | ||||
|         } | ||||
|         &[course-color="4"] .core-course-thumb { | ||||
|             background: var(--core-course-color-4); | ||||
|         } | ||||
|         &[course-color="5"] .core-course-thumb { | ||||
|             background: var(--core-course-color-5); | ||||
|         } | ||||
|         &[course-color="6"] .core-course-thumb { | ||||
|             background: var(--core-course-color-6); | ||||
|         } | ||||
|         &[course-color="7"] .core-course-thumb { | ||||
|             background: var(--core-course-color-7); | ||||
|         } | ||||
|         &[course-color="8"] .core-course-thumb { | ||||
|             background: var(--core-course-color-8); | ||||
|         } | ||||
|         &[course-color="9"] .core-course-thumb { | ||||
|             background: var(--core-course-color-9); | ||||
|         @for $i from 0 to length($core-course-image-background) { | ||||
|             &[course-color="#{$i}"] .core-course-thumb { | ||||
|                 background: nth($core-course-image-background, $i + 1); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         .core-course-thumb { | ||||
|  | ||||
| @ -63,8 +63,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|     async ngAfterViewInit(): Promise<void> { | ||||
|         await this.fetchInitialGrades(); | ||||
| 
 | ||||
|         this.grades.watchSplitViewOutlet(this.splitView); | ||||
|         this.grades.start(); | ||||
|         this.grades.start(this.splitView); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -42,8 +42,7 @@ export class CoreGradesCoursesPage implements OnDestroy, AfterViewInit { | ||||
|     async ngAfterViewInit(): Promise<void> { | ||||
|         await this.fetchInitialCourses(); | ||||
| 
 | ||||
|         this.courses.watchSplitViewOutlet(this.splitView); | ||||
|         this.courses.start(); | ||||
|         this.courses.start(this.splitView); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -31,6 +31,7 @@ import { CoreMenuItem, CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features regarding grades information. | ||||
| @ -51,16 +52,18 @@ export class CoreGradesHelperProvider { | ||||
|      * @return Formatted row object. | ||||
|      */ | ||||
|     protected formatGradeRow(tableRow: CoreGradesTableRow): CoreGradesFormattedRow { | ||||
|         const row = {}; | ||||
|         const row: CoreGradesFormattedRow = { | ||||
|             rowclass: '', | ||||
|         }; | ||||
|         for (const name in tableRow) { | ||||
|             if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) { | ||||
|                 let content = String(tableRow[name].content); | ||||
| 
 | ||||
|                 if (name == 'itemname') { | ||||
|                     this.setRowIcon(row, content); | ||||
|                     row['link'] = this.getModuleLink(content); | ||||
|                     row['rowclass'] += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; | ||||
|                     row['rowclass'] += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; | ||||
|                     row.link = this.getModuleLink(content); | ||||
|                     row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; | ||||
|                     row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; | ||||
| 
 | ||||
|                     content = content.replace(/<\/span>/gi, '\n'); | ||||
|                     content = CoreTextUtils.instance.cleanTags(content); | ||||
| @ -86,20 +89,20 @@ export class CoreGradesHelperProvider { | ||||
|      * @return Formatted row object. | ||||
|      */ | ||||
|     protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable { | ||||
|         const row = {}; | ||||
|         const row: CoreGradesFormattedRowForTable = {}; | ||||
|         for (let name in tableRow) { | ||||
|             if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) { | ||||
|                 let content = String(tableRow[name].content); | ||||
| 
 | ||||
|                 if (name == 'itemname') { | ||||
|                     row['id'] = parseInt(tableRow[name]!.id.split('_')[1], 10); | ||||
|                     row['colspan'] = tableRow[name]!.colspan; | ||||
|                     row['rowspan'] = (tableRow['leader'] && tableRow['leader'].rowspan) || 1; | ||||
|                     row.id = parseInt(tableRow[name]!.id.split('_')[1], 10); | ||||
|                     row.colspan = tableRow[name]!.colspan; | ||||
|                     row.rowspan = (tableRow.leader && tableRow.leader.rowspan) || 1; | ||||
| 
 | ||||
|                     this.setRowIcon(row, content); | ||||
|                     row['rowclass'] = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even'; | ||||
|                     row['rowclass'] += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; | ||||
|                     row['rowclass'] += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; | ||||
|                     row.rowclass = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even'; | ||||
|                     row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; | ||||
|                     row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; | ||||
| 
 | ||||
|                     content = content.replace(/<\/span>/gi, '\n'); | ||||
|                     content = CoreTextUtils.instance.cleanTags(content); | ||||
| @ -202,14 +205,14 @@ export class CoreGradesHelperProvider { | ||||
|      */ | ||||
|     async getGradesCourseData(grades: CoreGradesGradeOverview[]): Promise<CoreGradesGradeOverviewWithCourseData[]> { | ||||
|         // Obtain courses from cache to prevent network requests.
 | ||||
|         let coursesWereMissing; | ||||
|         let coursesWereMissing = false; | ||||
| 
 | ||||
|         try { | ||||
|             const courses = await CoreCourses.instance.getUserCourses(undefined, undefined, CoreSitesReadingStrategy.OnlyCache); | ||||
|             const coursesMap = CoreUtils.instance.arrayToObject(courses, 'id'); | ||||
| 
 | ||||
|             coursesWereMissing = this.addCourseData(grades, coursesMap); | ||||
|         } catch (error) { | ||||
|         } catch { | ||||
|             coursesWereMissing = true; | ||||
|         } | ||||
| 
 | ||||
| @ -278,7 +281,7 @@ export class CoreGradesHelperProvider { | ||||
|         const grades = await CoreGrades.instance.getCourseGradesTable(courseId, userId, siteId, ignoreCache); | ||||
| 
 | ||||
|         if (!grades) { | ||||
|             throw new Error('Couldn\'t get grade item'); | ||||
|             throw new CoreError('Couldn\'t get grade item'); | ||||
|         } | ||||
| 
 | ||||
|         return this.getGradesTableRow(grades, gradeId); | ||||
| @ -325,15 +328,15 @@ export class CoreGradesHelperProvider { | ||||
|         groupId?: number, | ||||
|         siteId?: string, | ||||
|         ignoreCache: boolean = false, | ||||
|     ): Promise<CoreGradesFormattedItem> { | ||||
|     ): Promise<CoreGradesFormattedItem[] | CoreGradesFormattedRow[]> { | ||||
|         const grades = await CoreGrades.instance.getGradeItems(courseId, userId, groupId, siteId, ignoreCache); | ||||
| 
 | ||||
|         if (!grades) { | ||||
|             throw new Error('Couldn\'t get grade module items'); | ||||
|             throw new CoreError('Couldn\'t get grade module items'); | ||||
|         } | ||||
| 
 | ||||
|         if ('tabledata' in grades) { | ||||
|             // Table format.
 | ||||
|             // 3.1 Table format.
 | ||||
|             return this.getModuleGradesTableRows(grades, moduleId); | ||||
|         } | ||||
| 
 | ||||
| @ -347,18 +350,16 @@ export class CoreGradesHelperProvider { | ||||
|      * @param selectedGrade Selected grade label. | ||||
|      * @return Selected grade value. | ||||
|      */ | ||||
|     getGradeValueFromLabel(grades: CoreMenuItem[], selectedGrade: string): number { | ||||
|     getGradeValueFromLabel(grades: CoreMenuItem[], selectedGrade?: string): number { | ||||
|         if (!grades || !selectedGrade) { | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         for (const x in grades) { | ||||
|             if (grades[x].label == selectedGrade) { | ||||
|                 return grades[x].value < 0 ? 0 : grades[x].value; | ||||
|             } | ||||
|         } | ||||
|         const grade = grades.find((grade) => grade.label == selectedGrade); | ||||
| 
 | ||||
|         return 0; | ||||
|         return !grade || grade.value < 0 | ||||
|             ? 0 | ||||
|             : grade.value; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -457,15 +458,15 @@ export class CoreGradesHelperProvider { | ||||
|             siteId = site.id; | ||||
|             currentUserId = site.getUserId(); | ||||
| 
 | ||||
|             if (moduleId) { | ||||
|                 // Try to open the module grade directly. Check if it's possible.
 | ||||
|                 const grades = await CoreGrades.instance.isGradeItemsAvalaible(siteId); | ||||
|             if (!moduleId) { | ||||
|                 throw new CoreError('Invalid moduleId'); | ||||
|             } | ||||
| 
 | ||||
|                 if (!grades) { | ||||
|                     throw new Error(); | ||||
|                 } | ||||
|             } else { | ||||
|                 throw new Error(); | ||||
|             // Try to open the module grade directly. Check if it's possible.
 | ||||
|             const grades = await CoreGrades.instance.isGradeItemsAvalaible(siteId); | ||||
| 
 | ||||
|             if (!grades) { | ||||
|                 throw new CoreError('No grades found.'); | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
| @ -476,7 +477,7 @@ export class CoreGradesHelperProvider { | ||||
|                 const item = Array.isArray(items) && items.find((item) => moduleId == item.cmid); | ||||
| 
 | ||||
|                 if (!item) { | ||||
|                     throw new Error(); | ||||
|                     throw new CoreError('Grade item not found.'); | ||||
|                 } | ||||
| 
 | ||||
|                 // Open the item directly.
 | ||||
| @ -560,46 +561,49 @@ export class CoreGradesHelperProvider { | ||||
|      * @param text HTML where the image will be rendered. | ||||
|      * @return Row object with the image. | ||||
|      */ | ||||
|     protected setRowIcon(row: CoreGradesFormattedRowForTable, text: string): CoreGradesFormattedRowForTable { | ||||
|     protected setRowIcon( | ||||
|         row: CoreGradesFormattedRowForTable | CoreGradesFormattedRow, | ||||
|         text: string, | ||||
|     ): CoreGradesFormattedRowForTable { | ||||
|         text = text.replace('%2F', '/').replace('%2f', '/'); | ||||
| 
 | ||||
|         if (text.indexOf('/agg_mean') > -1) { | ||||
|             row['itemtype'] = 'agg_mean'; | ||||
|             row['image'] = 'assets/img/grades/agg_mean.png'; | ||||
|             row.itemtype = 'agg_mean'; | ||||
|             row.image = 'assets/img/grades/agg_mean.png'; | ||||
|         } else if (text.indexOf('/agg_sum') > -1) { | ||||
|             row['itemtype'] = 'agg_sum'; | ||||
|             row['image'] = 'assets/img/grades/agg_sum.png'; | ||||
|             row.itemtype = 'agg_sum'; | ||||
|             row.image = 'assets/img/grades/agg_sum.png'; | ||||
|         } else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks')  > -1) { | ||||
|             row['itemtype'] = 'outcome'; | ||||
|             row['icon'] = 'fa-tasks'; | ||||
|             row.itemtype = 'outcome'; | ||||
|             row.icon = 'fas-chart-pie'; | ||||
|         } else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder')  > -1) { | ||||
|             row['itemtype'] = 'category'; | ||||
|             row['icon'] = 'fa-folder'; | ||||
|             row.itemtype = 'category'; | ||||
|             row.icon = 'fas-cubes'; | ||||
|         } else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o')  > -1) { | ||||
|             row['itemtype'] = 'manual'; | ||||
|             row['icon'] = 'fa-square-o'; | ||||
|             row.itemtype = 'manual'; | ||||
|             row.icon = 'far-square'; | ||||
|         } else if (text.indexOf('/mod/') > -1) { | ||||
|             const module = text.match(/mod\/([^/]*)\//); | ||||
|             if (typeof module?.[1] != 'undefined') { | ||||
|                 row['itemtype'] = 'mod'; | ||||
|                 row['itemmodule'] = module[1]; | ||||
|                 row['image'] = CoreCourse.instance.getModuleIconSrc( | ||||
|                 row.itemtype = 'mod'; | ||||
|                 row.itemmodule = module[1]; | ||||
|                 row.image = CoreCourse.instance.getModuleIconSrc( | ||||
|                     module[1], | ||||
|                     CoreDomUtils.instance.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined, | ||||
|                 ); | ||||
|             } | ||||
|         } else { | ||||
|             if (row['rowspan'] && row['rowspan'] > 1) { | ||||
|                 row['itemtype'] = 'category'; | ||||
|                 row['icon'] = 'fa-folder'; | ||||
|             if (row.rowspan && row.rowspan > 1) { | ||||
|                 row.itemtype = 'category'; | ||||
|                 row.icon = 'fas-cubes'; | ||||
|             } else if (text.indexOf('src=') > -1) { | ||||
|                 row['itemtype'] = 'unknown'; | ||||
|                 row.itemtype = 'unknown'; | ||||
|                 const src = text.match(/src="([^"]*)"/); | ||||
|                 row['image'] = src?.[1]; | ||||
|                 row.image = src?.[1]; | ||||
|             } else if (text.indexOf('<i ') > -1) { | ||||
|                 row['itemtype'] = 'unknown'; | ||||
|                 row.itemtype = 'unknown'; | ||||
|                 const src = text.match(/<i class="(?:[^"]*?\s)?(fa-[a-z0-9-]+)/); | ||||
|                 row['icon'] = src ? src[1] : ''; | ||||
|                 row.icon = src ? src[1] : ''; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @ -665,15 +669,53 @@ export class CoreGradesHelperProvider { | ||||
|         return Promise.resolve([]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Type guard to check if the param is a CoreGradesGradeItem. | ||||
|      * | ||||
|      * @param item Param to check. | ||||
|      * @return Whether the param is a CoreGradesGradeItem. | ||||
|      */ | ||||
|     isGradeItem(item: CoreGradesGradeItem | CoreGradesFormattedRow): item is CoreGradesGradeItem { | ||||
|         return 'outcomeid' in item; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class CoreGradesHelper extends makeSingleton(CoreGradesHelperProvider) {} | ||||
| 
 | ||||
| // @todo formatted data types.
 | ||||
| export type CoreGradesFormattedRow = any; | ||||
| export type CoreGradesFormattedRowForTable = any; | ||||
| export type CoreGradesFormattedItem = any; | ||||
| export type CoreGradesFormattedTableColumn = any; | ||||
| 
 | ||||
| export type CoreGradesFormattedItem = CoreGradesGradeItem & { | ||||
|     weight?: string; // Weight.
 | ||||
|     grade?: string; // The grade formatted.
 | ||||
|     range?: string; // Range formatted.
 | ||||
|     percentage?: string; // Percentage.
 | ||||
|     lettergrade?: string; // Letter grade.
 | ||||
|     average?: string; // Grade average.
 | ||||
| }; | ||||
| 
 | ||||
| export type CoreGradesFormattedRow = { | ||||
|     icon?: string; | ||||
|     link?: string | false; | ||||
|     rowclass?: string; | ||||
|     itemtype?: string; | ||||
|     image?: string; | ||||
|     itemmodule?: string; | ||||
|     rowspan?: number; | ||||
|     itemname?: string; // The item returned data.
 | ||||
|     weight?: string; // Weight column.
 | ||||
|     grade?: string; // Grade column.
 | ||||
|     range?: string;// Range column.
 | ||||
|     percentage?: string; // Percentage column.
 | ||||
|     lettergrade?: string; // Lettergrade column.
 | ||||
|     rank?: string; // Rank column.
 | ||||
|     average?: string; // Average column.
 | ||||
|     feedback?: string; // Feedback column.
 | ||||
|     contributiontocoursetotal?: string; // Contributiontocoursetotal column.
 | ||||
| }; | ||||
| 
 | ||||
| export type CoreGradesFormattedTableRow = CoreGradesFormattedTableRowFilled | CoreGradesFormattedTableRowEmpty; | ||||
| export type CoreGradesFormattedTable = { | ||||
|     columns: CoreGradesFormattedTableColumn[]; | ||||
|  | ||||
| @ -339,8 +339,10 @@ export class CoreGradesProvider { | ||||
|      * @return True if ws is avalaible, false otherwise. | ||||
|      * @since  Moodle 3.2 | ||||
|      */ | ||||
|     isGradeItemsAvalaible(siteId?: string): Promise<boolean> { | ||||
|         return CoreSites.instance.getSite(siteId).then((site) => site.wsAvailable('gradereport_user_get_grade_items')); | ||||
|     async isGradeItemsAvalaible(siteId?: string): Promise<boolean> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return site.wsAvailable('gradereport_user_get_grade_items'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -33,8 +33,7 @@ export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy { | ||||
|      */ | ||||
|     ngAfterViewInit(): void { | ||||
|         this.sections.setItems(CoreSettingsConstants.SECTIONS); | ||||
|         this.sections.watchSplitViewOutlet(this.splitView); | ||||
|         this.sections.start(); | ||||
|         this.sections.start(this.splitView); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -62,8 +62,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|     async ngAfterViewInit(): Promise<void> { | ||||
|         await this.fetchInitialParticipants(); | ||||
| 
 | ||||
|         this.participants.watchSplitViewOutlet(this.splitView); | ||||
|         this.participants.start(); | ||||
|         this.participants.start(this.splitView); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -267,7 +267,7 @@ export class CoreCronDelegateService { | ||||
|      * @return True if handler uses network or not defined, false otherwise. | ||||
|      */ | ||||
|     protected handlerUsesNetwork(name: string): boolean { | ||||
|         if (!this.handlers[name] || this.handlers[name].usesNetwork) { | ||||
|         if (!this.handlers[name] || !this.handlers[name].usesNetwork) { | ||||
|             // Invalid, return default.
 | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
| @ -412,7 +412,7 @@ export class CoreGroupsProvider { | ||||
|      * @param groupInfo Group info. | ||||
|      * @return Group ID to use. | ||||
|      */ | ||||
|     validateGroupId(groupId: number, groupInfo: CoreGroupInfo): number { | ||||
|     validateGroupId(groupId = 0, groupInfo: CoreGroupInfo): number { | ||||
|         if (groupId > 0 && groupInfo && groupInfo.groups && groupInfo.groups.length > 0) { | ||||
|             // Check if the group is in the list of groups.
 | ||||
|             if (groupInfo.groups.some((group) => groupId == group.id)) { | ||||
|  | ||||
| @ -243,12 +243,34 @@ export class CoreNavigatorService { | ||||
|         return CoreUrlUtils.instance.removeUrlParams(this.previousPath || ''); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Iterately get the params checking parent routes. | ||||
|      * | ||||
|      * @param route Current route. | ||||
|      * @param name Name of the parameter. | ||||
|      * @return Value of the parameter, undefined if not found. | ||||
|      */ | ||||
|     protected getRouteSnapshotParam<T = unknown>(name: string, route?: ActivatedRoute): T | undefined { | ||||
|         if (!route?.snapshot) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const value = route.snapshot.queryParams[name] ?? route.snapshot.params[name]; | ||||
| 
 | ||||
|         if (typeof value != 'undefined') { | ||||
|             return value; | ||||
|         } | ||||
| 
 | ||||
|         return this.getRouteSnapshotParam(name, route.parent || undefined); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a parameter for the current route. | ||||
|      * Please notice that objects can only be retrieved once. You must call this function only once per page and parameter, | ||||
|      * unless there's a new navigation to the page. | ||||
|      * | ||||
|      * @param name Name of the parameter. | ||||
|      * @param params Optional params to get the value from. If missing, it will autodetect. | ||||
|      * @return Value of the parameter, undefined if not found. | ||||
|      */ | ||||
|     getRouteParam<T = unknown>(name: string, params?: Params): T | undefined { | ||||
| @ -256,15 +278,16 @@ export class CoreNavigatorService { | ||||
| 
 | ||||
|         if (!params) { | ||||
|             const route = this.getCurrentRoute(); | ||||
|             if (!route.snapshot) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             value = route.snapshot.queryParams[name] ?? route.snapshot.params[name]; | ||||
|             value = this.getRouteSnapshotParam(name, route); | ||||
|         } else { | ||||
|             value = params[name]; | ||||
|         } | ||||
| 
 | ||||
|         if (typeof value == 'undefined') { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let storedParam = this.storedParams[value]; | ||||
| 
 | ||||
|         // Remove the parameter from our map if it's in there.
 | ||||
| @ -286,6 +309,7 @@ export class CoreNavigatorService { | ||||
|      * Angular router automatically converts numbers to string, this function automatically converts it back to number. | ||||
|      * | ||||
|      * @param name Name of the parameter. | ||||
|      * @param params Optional params to get the value from. If missing, it will autodetect. | ||||
|      * @return Value of the parameter, undefined if not found. | ||||
|      */ | ||||
|     getRouteNumberParam(name: string, params?: Params): number | undefined { | ||||
| @ -299,6 +323,7 @@ export class CoreNavigatorService { | ||||
|      * Angular router automatically converts booleans to string, this function automatically converts it back to boolean. | ||||
|      * | ||||
|      * @param name Name of the parameter. | ||||
|      * @param params Optional params to get the value from. If missing, it will autodetect. | ||||
|      * @return Value of the parameter, undefined if not found. | ||||
|      */ | ||||
|     getRouteBooleanParam(name: string, params?: Params): boolean | undefined { | ||||
| @ -355,6 +380,11 @@ export class CoreNavigatorService { | ||||
|         // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
 | ||||
|         // @todo this.location.replaceState('');
 | ||||
| 
 | ||||
|         options = { | ||||
|             preferCurrentTab: true, | ||||
|             ...options, | ||||
|         }; | ||||
| 
 | ||||
|         path = path.replace(/^(\.|\/main)?\//, ''); | ||||
| 
 | ||||
|         const pathRoot = /^[^/]+/.exec(path)?.[0] ?? ''; | ||||
| @ -364,7 +394,7 @@ export class CoreNavigatorService { | ||||
|             false, | ||||
|         ); | ||||
| 
 | ||||
|         if (options.preferCurrentTab === false && isMainMenuTab) { | ||||
|         if (!options.preferCurrentTab && isMainMenuTab) { | ||||
|             return this.navigate(`/main/${path}`, options); | ||||
|         } | ||||
| 
 | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user