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 { NgModule } from '@angular/core'; | ||||||
| 
 | 
 | ||||||
|  | import { AddonModAssignModule } from './assign/assign.module'; | ||||||
| import { AddonModBookModule } from './book/book.module'; | import { AddonModBookModule } from './book/book.module'; | ||||||
| import { AddonModLessonModule } from './lesson/lesson.module'; | import { AddonModLessonModule } from './lesson/lesson.module'; | ||||||
| import { AddonModPageModule } from './page/page.module'; | import { AddonModPageModule } from './page/page.module'; | ||||||
| @ -21,6 +22,7 @@ import { AddonModPageModule } from './page/page.module'; | |||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [], |     declarations: [], | ||||||
|     imports: [ |     imports: [ | ||||||
|  |         AddonModAssignModule, | ||||||
|         AddonModBookModule, |         AddonModBookModule, | ||||||
|         AddonModLessonModule, |         AddonModLessonModule, | ||||||
|         AddonModPageModule, |         AddonModPageModule, | ||||||
|  | |||||||
| @ -412,7 +412,7 @@ export class CoreGroupsProvider { | |||||||
|      * @param groupInfo Group info. |      * @param groupInfo Group info. | ||||||
|      * @return Group ID to use. |      * @return Group ID to use. | ||||||
|      */ |      */ | ||||||
|     validateGroupId(groupId: number, groupInfo: CoreGroupInfo): number { |     validateGroupId(groupId = 0, groupInfo: CoreGroupInfo): number { | ||||||
|         if (groupId > 0 && groupInfo && groupInfo.groups && groupInfo.groups.length > 0) { |         if (groupId > 0 && groupInfo && groupInfo.groups && groupInfo.groups.length > 0) { | ||||||
|             // Check if the group is in the list of groups.
 |             // Check if the group is in the list of groups.
 | ||||||
|             if (groupInfo.groups.some((group) => groupId == group.id)) { |             if (groupInfo.groups.some((group) => groupId == group.id)) { | ||||||
|  | |||||||
| @ -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.
 |         // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
 | ||||||
|         // @todo this.location.replaceState('');
 |         // @todo this.location.replaceState('');
 | ||||||
| 
 | 
 | ||||||
|  |         options = { | ||||||
|  |             preferCurrentTab: true, | ||||||
|  |             ...options, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|         path = path.replace(/^(\.|\/main)?\//, ''); |         path = path.replace(/^(\.|\/main)?\//, ''); | ||||||
| 
 | 
 | ||||||
|         const pathRoot = /^[^/]+/.exec(path)?.[0] ?? ''; |         const pathRoot = /^[^/]+/.exec(path)?.[0] ?? ''; | ||||||
| @ -389,7 +394,7 @@ export class CoreNavigatorService { | |||||||
|             false, |             false, | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if (options.preferCurrentTab === false && isMainMenuTab) { |         if (!options.preferCurrentTab && isMainMenuTab) { | ||||||
|             return this.navigate(`/main/${path}`, options); |             return this.navigate(`/main/${path}`, options); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user