forked from EVOgeek/Vmeda.Online
		
	MOBILE-3636 assign: Implement assignment base
This commit is contained in:
		
							parent
							
								
									b0cf681ab6
								
							
						
					
					
						commit
						a4eefeb25a
					
				
							
								
								
									
										42
									
								
								src/addons/mod/assign/assign-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/addons/mod/assign/assign-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| // (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 { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { AddonModAssignComponentsModule } from './components/components.module'; | ||||
| import { AddonModAssignIndexPage } from './pages/index/index.page'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: ':courseId/:cmId', | ||||
|         component: AddonModAssignIndexPage, | ||||
|     }, | ||||
|     { | ||||
|         path: ':courseId/:cmId/submission-list', | ||||
|         component: AddonModAssignSubmissionListPage, | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
|         CoreSharedModule, | ||||
|         AddonModAssignComponentsModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         AddonModAssignIndexPage, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignLazyModule {} | ||||
							
								
								
									
										66
									
								
								src/addons/mod/assign/assign.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/addons/mod/assign/assign.module.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 { 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 { 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'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: AddonModAssignModuleHandlerService.PAGE_NAME, | ||||
|         loadChildren: () => import('./assign-lazy.module').then(m => m.AddonModAssignLazyModule), | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreMainMenuTabRoutingModule.forChild(routes), | ||||
|         AddonModAssignComponentsModule, | ||||
|     ], | ||||
|     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 {} | ||||
							
								
								
									
										47
									
								
								src/addons/mod/assign/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/addons/mod/assign/components/components.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 { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { FormsModule } from '@angular/forms'; | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreCourseComponentsModule } from '@features/course/components/components.module'; | ||||
| import { AddonModAssignIndexComponent } from './index/index'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModAssignIndexComponent, | ||||
|         /* AddonModAssignSubmissionComponent, | ||||
|         AddonModAssignSubmissionPluginComponent, | ||||
|         AddonModAssignFeedbackPluginComponent*/ | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
|         IonicModule, | ||||
|         TranslateModule.forChild(), | ||||
|         FormsModule, | ||||
|         CoreSharedModule, | ||||
|         CoreCourseComponentsModule, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModAssignIndexComponent, | ||||
|         /* AddonModAssignSubmissionComponent, | ||||
|         AddonModAssignSubmissionPluginComponent, | ||||
|         AddonModAssignFeedbackPluginComponent */ | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignComponentsModule {} | ||||
| @ -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. --> | ||||
|     <!-- @todo <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId" | ||||
|         [moduleId]="module.id"> | ||||
|     </addon-mod-assign-submission>--> | ||||
| 
 | ||||
| </core-loading> | ||||
							
								
								
									
										414
									
								
								src/addons/mod/assign/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										414
									
								
								src/addons/mod/assign/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,414 @@ | ||||
| // (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, | ||||
| } from '../../services/assign'; | ||||
| import { AddonModAssignOffline } from '../../services/assign-offline'; | ||||
| import { | ||||
|     AddonModAssignAutoSyncData, | ||||
|     AddonModAssignSync, | ||||
|     AddonModAssignSyncProvider, | ||||
|     AddonModAssignSyncResult, | ||||
| } from '../../services/assign-sync'; | ||||
| 
 | ||||
| /** | ||||
|  * 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 { | ||||
| 
 | ||||
|     // @todo @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
 | ||||
|     submissionComponent?: any; | ||||
| 
 | ||||
|     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<any>(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<any>(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: number): 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 count Number of submissions with the status. | ||||
|      */ | ||||
|     goToSubmissionList(status: string, count: number): void { | ||||
|         if (typeof status != 'undefined' && !count && this.showNumbers) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const params: Params = { | ||||
|             groupId: this.group || 0, | ||||
|             moduleName: this.moduleName, | ||||
|         }; | ||||
|         if (typeof status != 'undefined') { | ||||
|             params.status = status; | ||||
|         } | ||||
|         CoreNavigator.instance.navigate('submission-list', { | ||||
|             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(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										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" | ||||
| } | ||||
							
								
								
									
										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(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										727
									
								
								src/addons/mod/assign/services/assign-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										727
									
								
								src/addons/mod/assign/services/assign-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,727 @@ | ||||
| // (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, | ||||
| } 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, inputData: any): void { | ||||
|         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: any = {}; | ||||
|         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: undefined, | ||||
|             gradeddate: undefined, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create an empty submission object. | ||||
|      * | ||||
|      * @return Submission. | ||||
|      */ | ||||
|     createEmptySubmission(): AddonModAssignSubmissionFormatted { | ||||
|         return { | ||||
|             id: undefined, | ||||
|             userid: undefined, | ||||
|             attemptnumber: undefined, | ||||
|             timecreated: undefined, | ||||
|             timemodified: undefined, | ||||
|             status: undefined, | ||||
|             groupid: undefined, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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): AddonModAssignPluginsEnabled { | ||||
|         const enabled: AddonModAssignPluginsEnabled = []; | ||||
| 
 | ||||
|         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, | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         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: any, | ||||
|     ): 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, | ||||
|         feedback: AddonModAssignSubmissionFeedback, | ||||
|         userId: number, | ||||
|     ): Promise<boolean> { | ||||
| 
 | ||||
|         let hasChanged = false; | ||||
| 
 | ||||
|         const promises = feedback.plugins | ||||
|             ? 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, | ||||
|         inputData: any, | ||||
|     ): Promise<boolean> { | ||||
|         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<any> { | ||||
| 
 | ||||
|         const pluginData = {}; | ||||
|         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, | ||||
|         inputData: any, | ||||
|         offline = false, | ||||
|     ): Promise<any> { | ||||
| 
 | ||||
|         const pluginData = {}; | ||||
|         const promises = submission.plugins | ||||
|             ? 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. | ||||
|      */ | ||||
|     uploadOrStoreFiles( | ||||
|         assignId: number, | ||||
|         folderName: string, | ||||
|         files: (CoreWSExternalFile | FileEntry)[], | ||||
|         offline = false, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<any> { | ||||
| 
 | ||||
|         if (offline) { | ||||
|             return this.storeSubmissionFiles(assignId, folderName, files, userId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         return this.uploadFiles(assignId, files, siteId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignHelper = makeSingleton(AddonModAssignHelperProvider); | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Assign submission with some calculated data. | ||||
|  */ | ||||
| export type AddonModAssignSubmissionFormatted = | ||||
|     Omit<AddonModAssignSubmission, 'id' | 'userid' | 'attemptnumber' | 'timecreated' | 'timemodified' | 'status' | 'groupid'> & { | ||||
|         id?: number; // Submission id.
 | ||||
|         userid?: number; // Student id.
 | ||||
|         attemptnumber?: number; // Attempt number.
 | ||||
|         timecreated?: number; // Submission creation time.
 | ||||
|         timemodified?: number; // Submission last modified time.
 | ||||
|         status?: string; // Submission status.
 | ||||
|         groupid?: number; // Group 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.
 | ||||
|     }; | ||||
| 
 | ||||
| /** | ||||
|  * Assingment subplugins type enabled. | ||||
|  */ | ||||
| export type AddonModAssignPluginsEnabled = { | ||||
|     type: string; // Plugin type.
 | ||||
| }[]; | ||||
| 
 | ||||
| /** | ||||
|  * Assingment 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({ assingid: 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({ assingid: 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 || '') || undefined; | ||||
|                     } | ||||
|                 } 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; | ||||
| }; | ||||
							
								
								
									
										1855
									
								
								src/addons/mod/assign/services/assign.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1855
									
								
								src/addons/mod/assign/services/assign.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										150
									
								
								src/addons/mod/assign/services/database/assign.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								src/addons/mod/assign/services/database/assign.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,150 @@ | ||||
| // (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; | ||||
| }; | ||||
							
								
								
									
										374
									
								
								src/addons/mod/assign/services/feedback-delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										374
									
								
								src/addons/mod/assign/services/feedback-delegate.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,374 @@ | ||||
| // (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 { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; | ||||
| import { AddonModAssignDefaultFeedbackHandler } from './handlers/default-feedback'; | ||||
| import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from './assign'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| 
 | ||||
| /** | ||||
|  * 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<any>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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): any | Promise<any>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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): any | Promise<any>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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: any, | ||||
|         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<any>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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: any, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<any>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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: any, siteId?: string): void | Promise<any>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 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<any | undefined> { | ||||
|         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<any | 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( | ||||
|         assignId: number, | ||||
|         userId: number, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         siteId?: string, | ||||
|     ): Promise<any | 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, | ||||
|         plugin: AddonModAssignPlugin, | ||||
|         inputData: any, | ||||
|         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<any> { | ||||
|         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: any, | ||||
|         siteId?: string, | ||||
|     ): Promise<any> { | ||||
| 
 | ||||
|         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: any, | ||||
|         siteId?: string, | ||||
|     ): Promise<any> { | ||||
|         return await this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'saveDraft', | ||||
|             [assignId, userId, plugin, inputData, siteId], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModAssignFeedbackDelegate = makeSingleton(AddonModAssignFeedbackDelegateService); | ||||
							
								
								
									
										146
									
								
								src/addons/mod/assign/services/handlers/default-feedback.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/addons/mod/assign/services/handlers/default-feedback.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,146 @@ | ||||
| // (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 { 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 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(): void { | ||||
|         // Nothing to do.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the draft saved data of the feedback plugin. | ||||
|      * | ||||
|      * @return Data (or promise resolved with the data). | ||||
|      */ | ||||
|     getDraft(): 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(): any[] { | ||||
|         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<any> { | ||||
|         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.
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										210
									
								
								src/addons/mod/assign/services/handlers/default-submission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/addons/mod/assign/services/handlers/default-submission.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,210 @@ | ||||
| // (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 { 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.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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(): 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(): any[] { | ||||
|         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<any> { | ||||
|         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); | ||||
							
								
								
									
										565
									
								
								src/addons/mod/assign/services/submission-delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										565
									
								
								src/addons/mod/assign/services/submission-delegate.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,565 @@ | ||||
| // (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 { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; | ||||
| import { AddonModAssignDefaultSubmissionHandler } from './handlers/default-submission'; | ||||
| import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from './assign'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| 
 | ||||
| /** | ||||
|  * 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: any, | ||||
|     ): 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: any, | ||||
|         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: any, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<any>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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, | ||||
|     ): any | Promise<any>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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: any, | ||||
|     ): 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: any, | ||||
|     ): 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: any, | ||||
|         pluginData: any, | ||||
|         offline?: boolean, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<any>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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: any, | ||||
|         pluginData: any, | ||||
|         siteId?: string, | ||||
|     ): void | Promise<any>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 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: any, | ||||
|     ): 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: any, | ||||
|         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: any, | ||||
|         siteId?: string, | ||||
|     ): Promise<any | undefined> { | ||||
|         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<any | 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: any, | ||||
|     ): 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: any, | ||||
|     ): 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: any, | ||||
|         pluginData: any, | ||||
|         offline?: boolean, | ||||
|         userId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<any | 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: any, | ||||
|         pluginData: any, | ||||
|         siteId?: string, | ||||
|     ): Promise<any | undefined> { | ||||
| 
 | ||||
|         return this.executeFunctionOnEnabled( | ||||
|             plugin.type, | ||||
|             'prepareSyncData', | ||||
|             [assign, submission, plugin, offlineData, pluginData, siteId], | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export class AddonModAssignSubmissionDelegate extends makeSingleton(AddonModAssignSubmissionDelegateService) {} | ||||
| @ -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, | ||||
|  | ||||
| @ -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)) { | ||||
|  | ||||
| @ -380,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] ?? ''; | ||||
| @ -389,7 +394,7 @@ export class CoreNavigatorService { | ||||
|             false, | ||||
|         ); | ||||
| 
 | ||||
|         if (options.preferCurrentTab === false && isMainMenuTab) { | ||||
|         if (!options.preferCurrentTab && isMainMenuTab) { | ||||
|             return this.navigate(`/main/${path}`, options); | ||||
|         } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user