MOBILE-3657 workshop: Submission and assessments
This commit is contained in:
		
							parent
							
								
									8db22cc54a
								
							
						
					
					
						commit
						b037c96b0b
					
				| @ -3,7 +3,7 @@ | ||||
| 
 | ||||
|     <span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span> | ||||
|     <core-rich-text-editor *ngIf="editMode" [control]="form.controls['f_'+field.id]" [placeholder]="field.name" | ||||
|         [formControlName]="'f_'+field.id" [component]="component" [componentId]="componentId" [autoSave]="true" | ||||
|         [name]="'f_'+field.id" [component]="component" [componentId]="componentId" [autoSave]="true" | ||||
|         contextLevel="module" [contextInstanceId]="componentId" [elementId]="'field_'+field.id" ngDefaultControl> | ||||
|     </core-rich-text-editor> | ||||
|     <core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors> | ||||
|  | ||||
| @ -0,0 +1,44 @@ | ||||
| // (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 { AddonModWorkshopAssessmentStrategyAccumulativeComponent } from './component/accumulative'; | ||||
| import { AddonModWorkshopAssessmentStrategyAccumulativeHandler } from './services/handler'; | ||||
| import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModWorkshopAssessmentStrategyAccumulativeComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonWorkshopAssessmentStrategyDelegate.registerHandler( | ||||
|                     AddonModWorkshopAssessmentStrategyAccumulativeHandler.instance, | ||||
|                 ); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModWorkshopAssessmentStrategyAccumulativeComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyAccumulativeModule {} | ||||
| @ -0,0 +1,25 @@ | ||||
| // (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 } from '@angular/core'; | ||||
| import { AddonModWorkshopAssessmentStrategyBaseComponent } from '../../../classes/assessment-strategy-component'; | ||||
| 
 | ||||
| /** | ||||
|  * Component for accumulative assessment strategy. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-workshop-assessment-strategy-accumulative', | ||||
|     templateUrl: 'addon-mod-workshop-assessment-strategy-accumulative.html', | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyAccumulativeComponent extends AddonModWorkshopAssessmentStrategyBaseComponent { } | ||||
| @ -0,0 +1,50 @@ | ||||
| <ng-container *ngFor="let field of assessment.form?.fields; let n = index"> | ||||
|     <ion-card *ngIf="n < assessment.form?.dimenssionscount"> | ||||
|         <ion-item class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ field.dimtitle }}</h2> | ||||
|                 <core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId" | ||||
|                     [courseId]="courseId"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|         <ion-item *ngIf="edit && field.grades"> | ||||
|             <ion-label position="stacked"> | ||||
|                 <span [core-mark-required]="true"> | ||||
|                 {{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }} | ||||
|                 </span> | ||||
|             </ion-label> | ||||
|             <ion-select [(ngModel)]="selectedValues[n].grade" interface="action-sheet"> | ||||
|                 <ion-select-option *ngFor="let grade of field.grades" [value]="grade.value">{{grade.label}}</ion-select-option> | ||||
|             </ion-select> | ||||
|             <core-input-errors *ngIf="fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]"> | ||||
|             </core-input-errors> | ||||
|         </ion-item> | ||||
|         <ion-item *ngIf="!edit && field.grades" class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }}</h2> | ||||
|                 <ng-container *ngFor="let grade of field.grades"> | ||||
|                     <p *ngIf="grade.value === selectedValues[n].grade">{{grade.label}}</p> | ||||
|                 </ng-container> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|         <ion-item *ngIf="edit"> | ||||
|             <ion-label position="stacked"> | ||||
|                 {{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} | ||||
|             </ion-label> | ||||
|             <ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" core-auto-rows></ion-textarea> | ||||
|         </ion-item> | ||||
|         <ion-item *ngIf="!edit" class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <h2> | ||||
|                     {{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} | ||||
|                 </h2> | ||||
|                 <p> | ||||
|                     <core-format-text [text]="selectedValues[n].peercomment" contextLevel="module" [contextInstanceId]="moduleId" | ||||
|                         [courseId]="courseId"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|     </ion-card> | ||||
| </ng-container> | ||||
| @ -0,0 +1,6 @@ | ||||
| { | ||||
|     "dimensioncommentfor": "Comment for {{$a}}", | ||||
|     "dimensiongradefor": "Grade for {{$a}}", | ||||
|     "dimensionnumber": "Aspect {{$a}}", | ||||
|     "mustchoosegrade": "You have to select a grade for this aspect" | ||||
| } | ||||
| @ -0,0 +1,151 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { | ||||
|     AddonModWorkshopAssessmentStrategyFieldErrors, | ||||
| } from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy'; | ||||
| import { | ||||
|     AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     AddonModWorkshopGetAssessmentFormFieldsParsedData, | ||||
| } from '@addons/mod/workshop/services/workshop'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreGradesHelper } from '@features/grades/services/grades-helper'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreFormFields } from '@singletons/form'; | ||||
| import { AddonWorkshopAssessmentStrategyHandler } from '../../../services/assessment-strategy-delegate'; | ||||
| import { AddonModWorkshopAssessmentStrategyAccumulativeComponent } from '../component/accumulative'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for accumulative assessment strategy plugin. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModWorkshopAssessmentStrategyAccumulativeHandlerService implements AddonWorkshopAssessmentStrategyHandler { | ||||
| 
 | ||||
|     name = 'AddonModWorkshopAssessmentStrategyAccumulative'; | ||||
|     strategyName = 'accumulative'; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getComponent(): Type<unknown> { | ||||
|         return AddonModWorkshopAssessmentStrategyAccumulativeComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async getOriginalValues( | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> { | ||||
|         const defaultGrade = Translate.instant('core.choosedots'); | ||||
|         const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = []; | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         form.fields.forEach((field, n) => { | ||||
|             field.dimtitle = Translate.instant('addon.mod_workshop_assessment_accumulative.dimensionnumber', { $a: field.number }); | ||||
| 
 | ||||
|             if (!form.current[n]) { | ||||
|                 form.current[n] = {}; | ||||
|             } | ||||
| 
 | ||||
|             originalValues[n] = {}; | ||||
|             originalValues[n].peercomment = form.current[n].peercomment || ''; | ||||
|             originalValues[n].number = field.number; // eslint-disable-line id-blacklist
 | ||||
| 
 | ||||
|             form.current[n].grade = form.current[n].grade ? parseInt(String(form.current[n].grade), 10) : -1; | ||||
| 
 | ||||
|             const gradingType = parseInt(String(field.grade), 10); | ||||
|             const dimension = form.dimensionsinfo.find((dimension) => dimension.id == parseInt(field.dimensionid, 10)); | ||||
|             const scale = dimension && gradingType < 0 ? dimension.scale : undefined; | ||||
| 
 | ||||
|             promises.push(CoreGradesHelper.makeGradesMenu(gradingType, undefined, defaultGrade, -1, scale).then((grades) => { | ||||
|                 field.grades = grades; | ||||
|                 originalValues[n].grade = form.current[n].grade; | ||||
| 
 | ||||
|                 return; | ||||
|             })); | ||||
|         }); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         return originalValues; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     hasDataChanged( | ||||
|         originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|     ): boolean { | ||||
|         for (const x in originalValues) { | ||||
|             if (originalValues[x].grade != currentValues[x].grade) { | ||||
|                 return true; | ||||
|             } | ||||
|             if (originalValues[x].peercomment != currentValues[x].peercomment) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async prepareAssessmentData( | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<CoreFormFields> { | ||||
|         const data: CoreFormFields =  {}; | ||||
|         const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {}; | ||||
|         let hasErrors = false; | ||||
| 
 | ||||
|         form.fields.forEach((field, idx) => { | ||||
|             if (idx < form.dimenssionscount) { | ||||
|                 const grade = parseInt(String(currentValues[idx].grade), 10); | ||||
|                 if (!isNaN(grade) && grade >= 0) { | ||||
|                     data['grade__idx_' + idx] = grade; | ||||
|                 } else { | ||||
|                     errors['grade_' + idx] = Translate.instant('addon.mod_workshop_assessment_accumulative.mustchoosegrade'); | ||||
|                     hasErrors = true; | ||||
|                 } | ||||
| 
 | ||||
|                 if (currentValues[idx].peercomment) { | ||||
|                     data['peercomment__idx_' + idx] = currentValues[idx].peercomment; | ||||
|                 } | ||||
| 
 | ||||
|                 data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; | ||||
|                 data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); | ||||
|                 data['weight__idx_' + idx] = parseInt(field.weight, 10) ||  0; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         if (hasErrors) { | ||||
|             throw errors; | ||||
|         } | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModWorkshopAssessmentStrategyAccumulativeHandler = | ||||
|     makeSingleton(AddonModWorkshopAssessmentStrategyAccumulativeHandlerService); | ||||
							
								
								
									
										29
									
								
								src/addons/mod/workshop/assessment/assessment.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/addons/mod/workshop/assessment/assessment.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| // (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 { AddonModWorkshopAssessmentStrategyAccumulativeModule } from './accumulative/accumulative.module'; | ||||
| import { AddonModWorkshopAssessmentStrategyCommentsModule } from './comments/comments.module'; | ||||
| import { AddonModWorkshopAssessmentStrategyNumErrorsModule } from './numerrors/numerrors.module'; | ||||
| import { AddonModWorkshopAssessmentStrategyRubricModule } from './rubric/rubric.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         AddonModWorkshopAssessmentStrategyAccumulativeModule, | ||||
|         AddonModWorkshopAssessmentStrategyCommentsModule, | ||||
|         AddonModWorkshopAssessmentStrategyNumErrorsModule, | ||||
|         AddonModWorkshopAssessmentStrategyRubricModule, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyModule {} | ||||
| @ -0,0 +1,44 @@ | ||||
| // (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 { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate'; | ||||
| import { AddonModWorkshopAssessmentStrategyCommentsComponent } from './component/comments'; | ||||
| import { AddonModWorkshopAssessmentStrategyCommentsHandler } from './services/handler'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModWorkshopAssessmentStrategyCommentsComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonWorkshopAssessmentStrategyDelegate.registerHandler( | ||||
|                     AddonModWorkshopAssessmentStrategyCommentsHandler.instance, | ||||
|                 ); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModWorkshopAssessmentStrategyCommentsComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyCommentsModule {} | ||||
| @ -0,0 +1,30 @@ | ||||
| <ng-container *ngFor="let field of assessment.form?.fields; let n = index"> | ||||
|     <ion-card *ngIf="n < assessment.form?.dimenssionscount"> | ||||
|         <ion-item class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ field.dimtitle }}</h2> | ||||
|                 <core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId" | ||||
|                     [courseId]="courseId"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|         <ion-item *ngIf="edit"> | ||||
|             <ion-label position="stacked"> | ||||
|                 <span [core-mark-required]="true"> | ||||
|                     {{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} | ||||
|                 </span> | ||||
|             </ion-label> | ||||
|             <ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" core-auto-rows></ion-textarea> | ||||
|             <core-input-errors *ngIf="fieldErrors['peercomment_' + n]" [errorText]="fieldErrors['peercomment_' + n]"> | ||||
|             </core-input-errors> | ||||
|         </ion-item> | ||||
|         <ion-item *ngIf="!edit" class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</h2> | ||||
|                 <p><core-format-text [text]="selectedValues[n].peercomment" contextLevel="module" [contextInstanceId]="moduleId" | ||||
|                     [courseId]="courseId"> | ||||
|                 </core-format-text></p> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|     </ion-card> | ||||
| </ng-container> | ||||
| @ -0,0 +1,25 @@ | ||||
| // (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 } from '@angular/core'; | ||||
| import { AddonModWorkshopAssessmentStrategyBaseComponent } from '../../../classes/assessment-strategy-component'; | ||||
| 
 | ||||
| /** | ||||
|  * Component for comments assessment strategy. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-workshop-assessment-strategy-comments', | ||||
|     templateUrl: 'addon-mod-workshop-assessment-strategy-comments.html', | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyCommentsComponent extends AddonModWorkshopAssessmentStrategyBaseComponent { } | ||||
							
								
								
									
										4
									
								
								src/addons/mod/workshop/assessment/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/addons/mod/workshop/assessment/comments/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| { | ||||
|     "dimensioncommentfor": "Comment for {{$a}}", | ||||
|     "dimensionnumber": "Aspect {{$a}}" | ||||
| } | ||||
							
								
								
									
										124
									
								
								src/addons/mod/workshop/assessment/comments/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/addons/mod/workshop/assessment/comments/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | ||||
| // (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 { | ||||
|     AddonModWorkshopAssessmentStrategyFieldErrors, | ||||
| } from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy'; | ||||
| import { AddonWorkshopAssessmentStrategyHandler } from '@addons/mod/workshop/services/assessment-strategy-delegate'; | ||||
| import { | ||||
|     AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     AddonModWorkshopGetAssessmentFormFieldsParsedData, | ||||
| } from '@addons/mod/workshop/services/workshop'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreFormFields } from '@singletons/form'; | ||||
| import { AddonModWorkshopAssessmentStrategyCommentsComponent } from '../component/comments'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for comments assessment strategy plugin. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModWorkshopAssessmentStrategyCommentsHandlerService implements AddonWorkshopAssessmentStrategyHandler { | ||||
| 
 | ||||
|     name = 'AddonModWorkshopAssessmentStrategyComments'; | ||||
|     strategyName = 'comments'; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getComponent(): Type<unknown> { | ||||
|         return AddonModWorkshopAssessmentStrategyCommentsComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async getOriginalValues( | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> { | ||||
|         const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = []; | ||||
| 
 | ||||
|         form.fields.forEach((field, n) => { | ||||
|             field.dimtitle = Translate.instant('addon.mod_workshop_assessment_comments.dimensionnumber', { $a: field.number }); | ||||
| 
 | ||||
|             if (!form.current[n]) { | ||||
|                 form.current[n] = {}; | ||||
|             } | ||||
| 
 | ||||
|             originalValues[n] = {}; | ||||
|             originalValues[n].peercomment = form.current[n].peercomment || ''; | ||||
|             originalValues[n].number = field.number; // eslint-disable-line id-blacklist
 | ||||
|         }); | ||||
| 
 | ||||
|         return originalValues; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     hasDataChanged( | ||||
|         originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|     ): boolean { | ||||
|         for (const x in originalValues) { | ||||
|             if (originalValues[x].peercomment != currentValues[x].peercomment) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async prepareAssessmentData( | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<CoreFormFields> { | ||||
|         const data: CoreFormFields =  {}; | ||||
|         const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {}; | ||||
|         let hasErrors = false; | ||||
| 
 | ||||
|         form.fields.forEach((field, idx) => { | ||||
|             if (idx < form.dimenssionscount) { | ||||
|                 if (currentValues[idx].peercomment) { | ||||
|                     data['peercomment__idx_' + idx] = currentValues[idx].peercomment; | ||||
|                 } else { | ||||
|                     errors['peercomment_' + idx] = Translate.instant('core.err_required'); | ||||
|                     hasErrors = true; | ||||
|                 } | ||||
| 
 | ||||
|                 data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; | ||||
|                 data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         if (hasErrors) { | ||||
|             throw errors; | ||||
|         } | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModWorkshopAssessmentStrategyCommentsHandler = | ||||
|     makeSingleton(AddonModWorkshopAssessmentStrategyCommentsHandlerService); | ||||
| @ -0,0 +1,53 @@ | ||||
| <ng-container *ngFor="let field of assessment.form?.fields; let n = index"> | ||||
|     <ion-card *ngIf="n < assessment.form?.dimenssionscount"> | ||||
|         <ion-item class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ field.dimtitle }}</h2> | ||||
|                 <core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId" | ||||
|                     [courseId]="courseId"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|         <ion-list> | ||||
|             <ion-radio-group [(ngModel)]="selectedValues[n].grade" [name]="'grade_' + n"> | ||||
|                 <ion-item> | ||||
|                     <ion-label position="stacked"> | ||||
|                         <span [core-mark-required]="edit"> | ||||
|                             {{ 'addon.mod_workshop.yourassessmentfor' | translate : {'$a': field.dimtitle } }} | ||||
|                         </span> | ||||
|                     </ion-label> | ||||
|                     <core-input-errors *ngIf="edit && fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]"> | ||||
|                     </core-input-errors> | ||||
|                 </ion-item> | ||||
|                 <ion-item> | ||||
|                     <ion-label> | ||||
|                         <core-format-text [text]="field.grade0" [filter]="false"></core-format-text> | ||||
|                     </ion-label> | ||||
|                     <ion-radio slot="start" [value]="-1" [disabled]="!edit"></ion-radio> | ||||
|                 </ion-item> | ||||
|                 <ion-item> | ||||
|                     <ion-label><core-format-text [text]="field.grade1" [filter]="false"></core-format-text></ion-label> | ||||
|                     <ion-radio slot="start" [value]="1" [disabled]="!edit"></ion-radio> | ||||
|                 </ion-item> | ||||
|             </ion-radio-group> | ||||
|         </ion-list> | ||||
|         <ion-item *ngIf="edit"> | ||||
|             <ion-label position="stacked"> | ||||
|                 {{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} | ||||
|             </ion-label> | ||||
|             <ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" [name]="'peercomment_' + n" | ||||
|                 core-auto-rows> | ||||
|             </ion-textarea> | ||||
|         </ion-item> | ||||
|         <ion-item *ngIf="!edit" class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</h2> | ||||
|                 <p> | ||||
|                     <core-format-text [text]="selectedValues[n].peercomment" contextLevel="module" [contextInstanceId]="moduleId" | ||||
|                         [courseId]="courseId"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|     </ion-card> | ||||
| </ng-container> | ||||
| @ -0,0 +1,25 @@ | ||||
| // (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 } from '@angular/core'; | ||||
| import { AddonModWorkshopAssessmentStrategyBaseComponent } from '../../../classes/assessment-strategy-component'; | ||||
| 
 | ||||
| /** | ||||
|  * Component for numerrors assessment strategy. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-workshop-assessment-strategy-numerrors', | ||||
|     templateUrl: 'addon-mod-workshop-assessment-strategy-numerrors.html', | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyNumErrorsComponent extends AddonModWorkshopAssessmentStrategyBaseComponent { } | ||||
							
								
								
									
										5
									
								
								src/addons/mod/workshop/assessment/numerrors/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/addons/mod/workshop/assessment/numerrors/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| { | ||||
|     "dimensioncommentfor": "Comment for {{$a}}", | ||||
|     "dimensiongradefor": "Grade for {{$a}}", | ||||
|     "dimensionnumber": "Assertion {{$a}}" | ||||
| } | ||||
| @ -0,0 +1,44 @@ | ||||
| // (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 { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate'; | ||||
| import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from './component/numerrors'; | ||||
| import { AddonModWorkshopAssessmentStrategyNumErrorsHandler } from './services/handler'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModWorkshopAssessmentStrategyNumErrorsComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonWorkshopAssessmentStrategyDelegate.registerHandler( | ||||
|                     AddonModWorkshopAssessmentStrategyNumErrorsHandler.instance, | ||||
|                 ); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModWorkshopAssessmentStrategyNumErrorsComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyNumErrorsModule {} | ||||
							
								
								
									
										134
									
								
								src/addons/mod/workshop/assessment/numerrors/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/addons/mod/workshop/assessment/numerrors/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,134 @@ | ||||
| // (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 { | ||||
|     AddonModWorkshopAssessmentStrategyFieldErrors, | ||||
| } from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy'; | ||||
| import { AddonWorkshopAssessmentStrategyHandler } from '@addons/mod/workshop/services/assessment-strategy-delegate'; | ||||
| import { | ||||
|     AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     AddonModWorkshopGetAssessmentFormFieldsParsedData, | ||||
| } from '@addons/mod/workshop/services/workshop'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { Translate, makeSingleton } from '@singletons'; | ||||
| import { CoreFormFields } from '@singletons/form'; | ||||
| import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from '../component/numerrors'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for numerrors assessment strategy plugin. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModWorkshopAssessmentStrategyNumErrorsHandlerService implements AddonWorkshopAssessmentStrategyHandler { | ||||
| 
 | ||||
|     name = 'AddonModWorkshopAssessmentStrategyNumErrors'; | ||||
|     strategyName = 'numerrors'; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getComponent(): Type<unknown> { | ||||
|         return AddonModWorkshopAssessmentStrategyNumErrorsComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async getOriginalValues( | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> { | ||||
|         const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = []; | ||||
| 
 | ||||
|         form.fields.forEach((field, n) => { | ||||
|             field.dimtitle = Translate.instant('addon.mod_workshop_assessment_numerrors.dimensionnumber', { $a: field.number }); | ||||
| 
 | ||||
|             if (!form.current[n]) { | ||||
|                 form.current[n] = {}; | ||||
|             } | ||||
| 
 | ||||
|             originalValues[n] = {}; | ||||
|             originalValues[n].peercomment = form.current[n].peercomment || ''; | ||||
|             originalValues[n].number = field.number; // eslint-disable-line id-blacklist
 | ||||
|             originalValues[n].grade  = form.current[n].grade || ''; | ||||
|         }); | ||||
| 
 | ||||
|         return originalValues; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     hasDataChanged( | ||||
|         originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|     ): boolean { | ||||
|         for (const x in originalValues) { | ||||
|             if (originalValues[x].grade != currentValues[x].grade) { | ||||
|                 return true; | ||||
|             } | ||||
|             if (originalValues[x].peercomment != currentValues[x].peercomment) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async prepareAssessmentData( | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<CoreFormFields> { | ||||
|         const data: CoreFormFields =  {}; | ||||
|         const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {}; | ||||
|         let hasErrors = false; | ||||
| 
 | ||||
|         form.fields.forEach((field, idx) => { | ||||
|             if (idx < form.dimenssionscount) { | ||||
|                 const grade = parseInt(String(currentValues[idx].grade), 10); | ||||
|                 if (!isNaN(grade) && (grade == 1 || grade == -1)) { | ||||
|                     data['grade__idx_' + idx] = grade; | ||||
|                 } else { | ||||
|                     errors['grade_' + idx] = Translate.instant('core.required'); | ||||
|                     hasErrors = true; | ||||
|                 } | ||||
| 
 | ||||
|                 if (currentValues[idx].peercomment) { | ||||
|                     data['peercomment__idx_' + idx] = currentValues[idx].peercomment; | ||||
|                 } | ||||
| 
 | ||||
|                 data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; | ||||
|                 data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); | ||||
|                 data['weight__idx_' + idx] = parseInt(field.weight, 10) ||  0; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         if (hasErrors) { | ||||
|             throw errors; | ||||
|         } | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModWorkshopAssessmentStrategyNumErrorsHandler = | ||||
|     makeSingleton(AddonModWorkshopAssessmentStrategyNumErrorsHandlerService); | ||||
| @ -0,0 +1,26 @@ | ||||
| <ng-container *ngFor="let field of assessment.form?.fields; let n = index"> | ||||
|     <ion-card *ngIf="n < assessment.form?.dimenssionscount"> | ||||
|         <ion-item class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <h2 [core-mark-required]="edit">{{ field.dimtitle }}</h2> | ||||
|                 <core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId" | ||||
|                     [courseId]="courseId"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|             <core-input-errors *ngIf="edit && fieldErrors['chosenlevelid_' + n]" [errorText]="fieldErrors['chosenlevelid_' + n]"> | ||||
|                 </core-input-errors> | ||||
|         </ion-item> | ||||
|         <ion-list> | ||||
|             <ion-radio-group [(ngModel)]="selectedValues[n].chosenlevelid" [name]="'chosenlevelid_' + n"> | ||||
|                 <ion-item *ngFor="let subfield of field.fields"> | ||||
|                     <ion-label> | ||||
|                         <p><core-format-text [text]="subfield.definition" contextLevel="module" | ||||
|                             [contextInstanceId]="moduleId" [courseId]="courseId"> | ||||
|                         </core-format-text></p> | ||||
|                     </ion-label> | ||||
|                     <ion-radio slot="start" [value]="subfield.levelid" [disabled]="!edit"></ion-radio> | ||||
|                 </ion-item> | ||||
|             </ion-radio-group> | ||||
|         </ion-list> | ||||
|     </ion-card> | ||||
| </ng-container> | ||||
| @ -0,0 +1,25 @@ | ||||
| // (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 } from '@angular/core'; | ||||
| import { AddonModWorkshopAssessmentStrategyBaseComponent } from '../../../classes/assessment-strategy-component'; | ||||
| 
 | ||||
| /** | ||||
|  * Component for rubric assessment strategy. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-workshop-assessment-strategy-rubric', | ||||
|     templateUrl: 'addon-mod-workshop-assessment-strategy-rubric.html', | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyRubricComponent extends AddonModWorkshopAssessmentStrategyBaseComponent { } | ||||
							
								
								
									
										4
									
								
								src/addons/mod/workshop/assessment/rubric/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/addons/mod/workshop/assessment/rubric/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| { | ||||
|     "dimensionnumber": "Criterion {{$a}}", | ||||
|     "mustchooseone": "You have to select one of these items" | ||||
| } | ||||
							
								
								
									
										44
									
								
								src/addons/mod/workshop/assessment/rubric/rubric.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/addons/mod/workshop/assessment/rubric/rubric.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| // (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 { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate'; | ||||
| import { AddonModWorkshopAssessmentStrategyRubricComponent } from './component/rubric'; | ||||
| import { AddonModWorkshopAssessmentStrategyRubricHandler } from './services/handler'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModWorkshopAssessmentStrategyRubricComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 AddonWorkshopAssessmentStrategyDelegate.registerHandler( | ||||
|                     AddonModWorkshopAssessmentStrategyRubricHandler.instance, | ||||
|                 ); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModWorkshopAssessmentStrategyRubricComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyRubricModule {} | ||||
							
								
								
									
										125
									
								
								src/addons/mod/workshop/assessment/rubric/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/addons/mod/workshop/assessment/rubric/services/handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,125 @@ | ||||
| // (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 { | ||||
|     AddonModWorkshopAssessmentStrategyFieldErrors, | ||||
| } from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy'; | ||||
| import { AddonWorkshopAssessmentStrategyHandler } from '@addons/mod/workshop/services/assessment-strategy-delegate'; | ||||
| import { | ||||
|     AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     AddonModWorkshopGetAssessmentFormFieldsParsedData, | ||||
| } from '@addons/mod/workshop/services/workshop'; | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { Translate, makeSingleton } from '@singletons'; | ||||
| import { CoreFormFields } from '@singletons/form'; | ||||
| import { AddonModWorkshopAssessmentStrategyRubricComponent } from '../component/rubric'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for rubric assessment strategy plugin. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonModWorkshopAssessmentStrategyRubricHandlerService implements AddonWorkshopAssessmentStrategyHandler { | ||||
| 
 | ||||
|     name = 'AddonModWorkshopAssessmentStrategyRubric'; | ||||
|     strategyName = 'rubric'; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getComponent(): Type<unknown> { | ||||
|         return AddonModWorkshopAssessmentStrategyRubricComponent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async getOriginalValues( | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> { | ||||
|         const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = []; | ||||
| 
 | ||||
|         form.fields.forEach((field, n) => { | ||||
|             field.dimtitle = Translate.instant('addon.mod_workshop_assessment_rubric.dimensionnumber', { $a: field.number }); | ||||
| 
 | ||||
|             if (!form.current[n]) { | ||||
|                 form.current[n] = {}; | ||||
|             } | ||||
| 
 | ||||
|             originalValues[n] = {}; | ||||
|             originalValues[n].chosenlevelid = form.current[n].chosenlevelid || ''; | ||||
|             originalValues[n].number = field.number; // eslint-disable-line id-blacklist
 | ||||
|         }); | ||||
| 
 | ||||
|         return originalValues; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     hasDataChanged( | ||||
|         originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|     ): boolean { | ||||
|         for (const x in originalValues) { | ||||
|             if (originalValues[x].chosenlevelid != (currentValues[x].chosenlevelid || '')) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async prepareAssessmentData( | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<CoreFormFields> { | ||||
|         const data: CoreFormFields =  {}; | ||||
|         const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {}; | ||||
|         let hasErrors = false; | ||||
| 
 | ||||
|         form.fields.forEach((field, idx) => { | ||||
|             if (idx < form.dimenssionscount) { | ||||
|                 const id = parseInt(currentValues[idx].chosenlevelid, 10); | ||||
|                 if (!isNaN(id) && id >= 0) { | ||||
|                     data['chosenlevelid__idx_' + idx] = id; | ||||
|                 } else { | ||||
|                     errors['chosenlevelid_' + idx] = Translate.instant('addon.mod_workshop_assessment_rubric.mustchooseone'); | ||||
|                     hasErrors = true; | ||||
|                 } | ||||
| 
 | ||||
|                 data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; | ||||
|                 data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         if (hasErrors) { | ||||
|             throw errors; | ||||
|         } | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonModWorkshopAssessmentStrategyRubricHandler = | ||||
|     makeSingleton(AddonModWorkshopAssessmentStrategyRubricHandlerService); | ||||
| @ -0,0 +1,36 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Input } from '@angular/core'; | ||||
| import { AddonModWorkshopGetAssessmentFormFieldsParsedData } from '../services/workshop'; | ||||
| import { AddonModWorkshopSubmissionAssessmentWithFormData } from '../services/workshop-helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Base class for component to render an assessment strategy. | ||||
|  */ | ||||
| @Component({ | ||||
|     template: '', | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyBaseComponent { | ||||
| 
 | ||||
|     @Input() workshopId!: number; | ||||
|     @Input() assessment!: AddonModWorkshopSubmissionAssessmentWithFormData; | ||||
|     @Input() edit!: boolean; | ||||
|     @Input() selectedValues!: AddonModWorkshopGetAssessmentFormFieldsParsedData[]; | ||||
|     @Input() fieldErrors!: Record<string, string>; | ||||
|     @Input() strategy!: string; | ||||
|     @Input() moduleId!: number; | ||||
|     @Input() courseId?: number; | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,68 @@ | ||||
| <h3 class="ion-padding">{{ 'addon.mod_workshop.assessmentform' | translate }}</h3> | ||||
| 
 | ||||
| <form name="mma-mod_workshop-assessment-form" #assessmentForm> | ||||
|     <core-loading [hideUntil]="assessmentStrategyLoaded"> | ||||
|         <ng-container *ngIf="componentClass && assessmentStrategyLoaded"> | ||||
|             <core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component> | ||||
|         </ng-container> | ||||
| 
 | ||||
|         <ion-card class="core-info-card" *ngIf="notSupported"> | ||||
|             <ion-item> | ||||
|                 <ion-label> | ||||
|                     <p>{{ 'addon.mod_workshop.assessmentstrategynotsupported' | translate:{$a: strategy} }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <ion-card *ngIf="assessmentStrategyLoaded && overallFeedkback && | ||||
|             (edit || data.assessment?.feedbackauthor || data.assessment?.feedbackattachmentfiles?.length) "> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label><h2>{{ 'addon.mod_workshop.overallfeedback' | translate }}</h2></ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item position="stacked" *ngIf="edit"> | ||||
|                 <ion-label position="stacked"> | ||||
|                     <span [core-mark-required]="overallFeedkbackRequired"> | ||||
|                         {{ 'addon.mod_workshop.feedbackauthor' | translate }} | ||||
|                     </span> | ||||
|                 </ion-label> | ||||
|                 <core-rich-text-editor [control]="feedbackControl" [component]="component" | ||||
|                     [componentId]="workshop.coursemodule" [autoSave]="true" contextLevel="module" | ||||
|                     [contextInstanceId]="workshop.coursemodule" elementId="feedbackauthor_editor" | ||||
|                     [draftExtraParams]="{asid: assessmentId}" (contentChanged)="onFeedbackChange($event)"> | ||||
|                 </core-rich-text-editor> | ||||
|                 <core-input-errors | ||||
|                     *ngIf="overallFeedkbackRequired && data.fieldErrors && data.fieldErrors['feedbackauthor']" | ||||
|                     [errorText]="data.fieldErrors['feedbackauthor']"> | ||||
|                 </core-input-errors> | ||||
|             </ion-item> | ||||
|             <core-attachments *ngIf="edit && workshop.overallfeedbackfiles" [files]="data.assessment?.feedbackattachmentfiles" | ||||
|                 [maxSize]="workshop.overallfeedbackmaxbytes" [maxSubmissions]="workshop.overallfeedbackfiles" | ||||
|                 [component]="component" [componentId]="componentId" [allowOffline]="true"> | ||||
|             </core-attachments> | ||||
|             <ion-item *ngIf="edit && access && access.canallocate"> | ||||
|                 <ion-label position="stacked"> | ||||
|                     <span [core-mark-required]="true"> | ||||
|                         {{ 'addon.mod_workshop.assessmentweight' | translate }} | ||||
|                     </span> | ||||
|                 </ion-label> | ||||
|                 <ion-select [(ngModel)]="weight" interface="action-sheet" name="weight"> | ||||
|                     <ion-select-option *ngFor="let w of weights" [value]="w">{{w}}</ion-select-option> | ||||
|                 </ion-select> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="!edit && data.assessment?.feedbackauthor"> | ||||
|                 <ion-label> | ||||
|                     <core-format-text [component]="component" [componentId]="componentId" [text]="data.assessment?.feedbackauthor" | ||||
|                         contextLevel="module" [contextInstanceId]="workshop.coursemodule" [courseId]="workshop.course"> | ||||
|                     </core-format-text> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item *ngIf="!edit && workshop.overallfeedbackfiles && data.assessment?.feedbackattachmentfiles?.length" | ||||
|                 lines="none"> | ||||
|                 <ion-label> | ||||
|                     <core-files [files]="data.assessment?.feedbackattachmentfiles" [component]="component" | ||||
|                         [componentId]="componentId"></core-files> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
|     </core-loading> | ||||
| </form> | ||||
| @ -0,0 +1,426 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Input, OnInit, ViewChild, ElementRef, Type, OnDestroy } from '@angular/core'; | ||||
| import { FormControl } from '@angular/forms'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; | ||||
| import { CoreFileSession } from '@services/file-session'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreFormFields, CoreForms } from '@singletons/form'; | ||||
| import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate'; | ||||
| import { | ||||
|     AddonModWorkshopProvider, | ||||
|     AddonModWorkshopOverallFeedbackMode, | ||||
|     AddonModWorkshop, | ||||
|     AddonModWorkshopData, | ||||
|     AddonModWorkshopGetWorkshopAccessInformationWSResponse, | ||||
|     AddonModWorkshopGetAssessmentFormFieldsParsedData, | ||||
| } from '../../services/workshop'; | ||||
| import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper'; | ||||
| import { AddonModWorkshopOffline } from '../../services/workshop-offline'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays workshop assessment strategy form. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-workshop-assessment-strategy', | ||||
|     templateUrl: 'addon-mod-workshop-assessment-strategy.html', | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @Input() workshop!: AddonModWorkshopData; | ||||
|     @Input() access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse; | ||||
|     @Input() assessmentId!: number; | ||||
|     @Input() userId!: number; | ||||
|     @Input() strategy!: string; | ||||
|     @Input() edit = false; | ||||
| 
 | ||||
|     @ViewChild('assessmentForm') formElement!: ElementRef; | ||||
| 
 | ||||
|     componentClass?: Type<unknown>; | ||||
|     data: AddonModWorkshopAssessmentStrategyData = { | ||||
|         workshopId: 0, | ||||
|         assessment: undefined, | ||||
|         edit: false, | ||||
|         selectedValues: [], | ||||
|         fieldErrors: {}, | ||||
|         strategy: '', | ||||
|         moduleId: 0, | ||||
|         courseId: undefined, | ||||
|     }; | ||||
| 
 | ||||
|     assessmentStrategyLoaded = false; | ||||
|     notSupported = false; | ||||
|     feedbackText = ''; | ||||
|     feedbackControl = new FormControl(); | ||||
|     overallFeedkback = false; | ||||
|     overallFeedkbackRequired = false; | ||||
|     component = AddonModWorkshopProvider.COMPONENT; | ||||
|     componentId?: number; | ||||
|     weights: number[] = []; | ||||
|     weight?: number; | ||||
| 
 | ||||
|     protected obsInvalidated?: CoreEventObserver; | ||||
|     protected hasOffline = false; | ||||
|     protected originalData: { | ||||
|         text: string; | ||||
|         files: CoreFileEntry[]; | ||||
|         weight: number; | ||||
|         selectedValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[]; | ||||
|     } = { | ||||
|         text: '', | ||||
|         files: [], | ||||
|         weight: 1, | ||||
|         selectedValues: [], | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         if (!this.assessmentId || !this.strategy) { | ||||
|             this.assessmentStrategyLoaded = true; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.data.workshopId = this.workshop.id; | ||||
|         this.data.edit = this.edit; | ||||
|         this.data.strategy = this.strategy; | ||||
|         this.data.moduleId = this.workshop.coursemodule; | ||||
|         this.data.courseId = this.workshop.course; | ||||
| 
 | ||||
|         this.componentClass = AddonWorkshopAssessmentStrategyDelegate.getComponentForPlugin(this.strategy); | ||||
|         if (this.componentClass) { | ||||
|             this.overallFeedkback = this.workshop.overallfeedbackmode != AddonModWorkshopOverallFeedbackMode.DISABLED; | ||||
|             this.overallFeedkbackRequired = | ||||
|                 this.workshop.overallfeedbackmode == AddonModWorkshopOverallFeedbackMode.ENABLED_REQUIRED; | ||||
|             this.componentId = this.workshop.coursemodule; | ||||
| 
 | ||||
|             // Load Weights selector.
 | ||||
|             if (this.edit && this.access.canallocate) { | ||||
|                 this.weights; | ||||
|                 for (let i = 16; i >= 0; i--) { | ||||
|                     this.weights[i] = i; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Check if rich text editor is enabled.
 | ||||
|             if (this.edit) { | ||||
|                 // Block the workshop.
 | ||||
|                 CoreSync.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshop.id); | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 await this.load(); | ||||
|                 this.obsInvalidated = CoreEvents.on( | ||||
|                     AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, | ||||
|                     this.load.bind(this), | ||||
| 
 | ||||
|                     CoreSites.getCurrentSiteId(), | ||||
|                 ); | ||||
|             } catch (error) { | ||||
|                 this.componentClass = undefined; | ||||
|                 CoreDomUtils.showErrorModalDefault(error, 'Error loading assessment.'); | ||||
|             } finally { | ||||
|                 this.assessmentStrategyLoaded = true; | ||||
|             } | ||||
|         } else { | ||||
|             // Helper data and fallback.
 | ||||
|             this.notSupported = !AddonWorkshopAssessmentStrategyDelegate.isPluginSupported(this.strategy); | ||||
|             this.assessmentStrategyLoaded = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to load the assessment data. | ||||
|      * | ||||
|      * @return Promised resvoled when data is loaded. | ||||
|      */ | ||||
|     protected async load(): Promise<void> { | ||||
|         this.data.assessment = await AddonModWorkshopHelper.getReviewerAssessmentById(this.workshop.id, this.assessmentId, { | ||||
|             userId: this.userId, | ||||
|             cmId: this.workshop.coursemodule, | ||||
|         }); | ||||
| 
 | ||||
|         if (this.edit) { | ||||
|             try { | ||||
|                 const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId); | ||||
|                 const offlineData = offlineAssessment.inputdata; | ||||
| 
 | ||||
|                 this.hasOffline = true; | ||||
| 
 | ||||
|                 this.data.assessment.feedbackauthor = <string>offlineData.feedbackauthor; | ||||
| 
 | ||||
|                 if (this.access.canallocate) { | ||||
|                     this.data.assessment.weight = <number>offlineData.weight; | ||||
|                 } | ||||
| 
 | ||||
|                 // Override assessment plugins values.
 | ||||
|                 this.data.assessment.form!.current = AddonModWorkshop.parseFields( | ||||
|                     CoreUtils.objectToArrayOfObjects(offlineData, 'name', 'value'), | ||||
|                 ); | ||||
| 
 | ||||
|                 // Override offline files.
 | ||||
|                 if (offlineData) { | ||||
|                     this.data.assessment.feedbackattachmentfiles = | ||||
|                         await AddonModWorkshopHelper.getAssessmentFilesFromOfflineFilesObject( | ||||
|                             <CoreFileUploaderStoreFilesResult>offlineData.feedbackauthorattachmentsid, | ||||
|                             this.workshop.id, | ||||
|                             this.assessmentId, | ||||
|                         ); | ||||
|                 } | ||||
|             } catch { | ||||
|                 this.hasOffline = false; | ||||
|                 // Ignore errors.
 | ||||
|             } finally { | ||||
|                 this.feedbackText = this.data.assessment.feedbackauthor; | ||||
|                 this.feedbackControl.setValue(this.feedbackText); | ||||
| 
 | ||||
|                 this.originalData.text = this.data.assessment.feedbackauthor; | ||||
| 
 | ||||
|                 if (this.access.canallocate) { | ||||
|                     this.originalData.weight = this.data.assessment.weight; | ||||
|                 } | ||||
| 
 | ||||
|                 this.originalData.files = []; | ||||
|                 this.data.assessment.feedbackattachmentfiles.forEach((file) => { | ||||
|                     let filename = CoreFile.getFileName(file); | ||||
|                     if (!filename) { | ||||
|                         // We don't have filename, extract it from the path.
 | ||||
|                         filename = CoreFileHelper.getFilenameFromPath(file) || ''; | ||||
|                     } | ||||
| 
 | ||||
|                     this.originalData.files.push({ | ||||
|                         filename, | ||||
|                         fileurl: '', // No needed to compare.
 | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             this.data.selectedValues = await AddonWorkshopAssessmentStrategyDelegate.getOriginalValues( | ||||
|                 this.strategy, | ||||
|                 this.data.assessment.form!, | ||||
|                 this.workshop.id, | ||||
|             ); | ||||
|         } finally { | ||||
|             this.originalData.selectedValues = CoreUtils.clone(this.data.selectedValues); | ||||
|             if (this.edit) { | ||||
|                 CoreFileSession.setFiles( | ||||
|                     AddonModWorkshopProvider.COMPONENT, | ||||
|                     this.workshop.id + '_' + this.assessmentId, | ||||
|                     this.data.assessment.feedbackattachmentfiles, | ||||
|                 ); | ||||
|                 if (this.access.canallocate) { | ||||
|                     this.weight = this.data.assessment.weight; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if data has changed. | ||||
|      * | ||||
|      * @return True if data has changed. | ||||
|      */ | ||||
|     hasDataChanged(): boolean { | ||||
|         if (!this.assessmentStrategyLoaded) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // Compare feedback text.
 | ||||
|         const text = CoreTextUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []); | ||||
|         if (this.originalData.text != text) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (this.access.canallocate && this.originalData.weight != this.weight) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // Compare feedback files.
 | ||||
|         const files = CoreFileSession.getFiles( | ||||
|             AddonModWorkshopProvider.COMPONENT, | ||||
|             this.workshop.id + '_' + this.assessmentId, | ||||
|         ) || []; | ||||
|         if (CoreFileUploader.areFileListDifferent(files, this.originalData.files)) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return AddonWorkshopAssessmentStrategyDelegate.hasDataChanged( | ||||
|             this.workshop.strategy!, | ||||
|             this.originalData.selectedValues, | ||||
|             this.data.selectedValues, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the assessment. | ||||
|      * | ||||
|      * @return Promise resolved when done, rejected if assessment could not be saved. | ||||
|      */ | ||||
|     async saveAssessment(): Promise<void> { | ||||
|         const files = CoreFileSession.getFiles( | ||||
|             AddonModWorkshopProvider.COMPONENT, | ||||
|             this.workshop.id + '_' + this.assessmentId, | ||||
|         ) || []; | ||||
| 
 | ||||
|         let saveOffline = false; | ||||
|         let allowOffline = !files.length; | ||||
| 
 | ||||
|         const modal = await CoreDomUtils.showModalLoading('core.sending', true); | ||||
| 
 | ||||
|         this.data.fieldErrors = {}; | ||||
| 
 | ||||
|         try { | ||||
|             let attachmentsId: CoreFileUploaderStoreFilesResult | number; | ||||
|             try { | ||||
|                 // Upload attachments first if any.
 | ||||
|                 attachmentsId = await AddonModWorkshopHelper.uploadOrStoreAssessmentFiles( | ||||
|                     this.workshop.id, | ||||
|                     this.assessmentId, | ||||
|                     files, | ||||
|                     saveOffline, | ||||
|                 ); | ||||
|             } catch { | ||||
|                 // Cannot upload them in online, save them in offline.
 | ||||
|                 saveOffline = true; | ||||
|                 allowOffline = true; | ||||
| 
 | ||||
|                 attachmentsId = await AddonModWorkshopHelper.uploadOrStoreAssessmentFiles( | ||||
|                     this.workshop.id, | ||||
|                     this.assessmentId, | ||||
|                     files, | ||||
|                     saveOffline, | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             const text = CoreTextUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []); | ||||
| 
 | ||||
|             let assessmentData: CoreFormFields<unknown>; | ||||
|             try { | ||||
|                 assessmentData = await AddonModWorkshopHelper.prepareAssessmentData( | ||||
|                     this.workshop, | ||||
|                     this.data.selectedValues, | ||||
|                     text, | ||||
|                     this.data.assessment!.form!, | ||||
|                     attachmentsId, | ||||
|                 ); | ||||
|             } catch (errors) { | ||||
|                 this.data.fieldErrors = errors; | ||||
| 
 | ||||
|                 throw new CoreError(Translate.instant('core.errorinvalidform')); | ||||
|             } | ||||
| 
 | ||||
|             let gradeUpdated = false; | ||||
|             if (saveOffline) { | ||||
|                 // Save assessment in offline.
 | ||||
|                 await AddonModWorkshopOffline.saveAssessment( | ||||
|                     this.workshop.id, | ||||
|                     this.assessmentId, | ||||
|                     this.workshop.course, | ||||
|                     assessmentData, | ||||
|                 ); | ||||
| 
 | ||||
|                 gradeUpdated = false; | ||||
|             } else { | ||||
| 
 | ||||
|                 // Try to send it to server.
 | ||||
|                 // Don't allow offline if there are attachments since they were uploaded fine.
 | ||||
|                 gradeUpdated = await AddonModWorkshop.updateAssessment( | ||||
|                     this.workshop.id, | ||||
|                     this.assessmentId, | ||||
|                     this.workshop.course, | ||||
|                     assessmentData, | ||||
|                     undefined, | ||||
|                     allowOffline, | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             CoreForms.triggerFormSubmittedEvent(this.formElement, !!gradeUpdated, CoreSites.getCurrentSiteId()); | ||||
| 
 | ||||
|             const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|             // If sent to the server, invalidate and clean.
 | ||||
|             if (gradeUpdated) { | ||||
|                 promises.push(AddonModWorkshopHelper.deleteAssessmentStoredFiles(this.workshop.id, this.assessmentId)); | ||||
|                 promises.push(AddonModWorkshop.invalidateAssessmentFormData(this.workshop.id, this.assessmentId)); | ||||
|                 promises.push(AddonModWorkshop.invalidateAssessmentData(this.workshop.id, this.assessmentId)); | ||||
|             } | ||||
| 
 | ||||
|             await CoreUtils.ignoreErrors(Promise.all(promises)); | ||||
| 
 | ||||
|             CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, { | ||||
|                 workshopId: this.workshop.id, | ||||
|                 assessmentId: this.assessmentId, | ||||
|                 userId: CoreSites.getCurrentSiteUserId(), | ||||
|             }, CoreSites.getCurrentSiteId()); | ||||
| 
 | ||||
|             if (files) { | ||||
|                 // Delete the local files from the tmp folder.
 | ||||
|                 CoreFileUploader.clearTmpFiles(files); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error saving assessment.'); | ||||
|         } finally { | ||||
|             modal.dismiss(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Feedback text changed. | ||||
|      * | ||||
|      * @param text The new text. | ||||
|      */ | ||||
|     onFeedbackChange(text: string): void { | ||||
|         this.feedbackText = text; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.obsInvalidated?.off(); | ||||
| 
 | ||||
|         if (this.data.assessment?.feedbackattachmentfiles) { | ||||
|             // Delete the local files from the tmp folder.
 | ||||
|             CoreFileUploader.clearTmpFiles(this.data.assessment.feedbackattachmentfiles); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type AddonModWorkshopAssessmentStrategyData = { | ||||
|     workshopId: number; | ||||
|     assessment?: AddonModWorkshopSubmissionAssessmentWithFormData; | ||||
|     edit: boolean; | ||||
|     selectedValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[]; | ||||
|     fieldErrors: AddonModWorkshopAssessmentStrategyFieldErrors; | ||||
|     strategy: string; | ||||
|     moduleId: number; | ||||
|     courseId?: number; | ||||
| }; | ||||
| 
 | ||||
| export type AddonModWorkshopAssessmentStrategyFieldErrors = Record<string, string>; | ||||
| @ -0,0 +1,27 @@ | ||||
| <core-loading [hideUntil]="loaded"> | ||||
|     <ion-item class="ion-text-wrap" [detail]="canViewAssessment && !canSelfAssess" (click)="gotoAssessment($event)"> | ||||
|         <core-user-avatar [user]="profile" slot="start" [courseId]="courseId" [userId]="profile?.id"></core-user-avatar> | ||||
|         <ion-label> | ||||
|             <h2 *ngIf="profile && profile.fullname">{{profile.fullname}}</h2> | ||||
|             <p *ngIf="showGrade(assessment.grade)"> | ||||
|                 {{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}} | ||||
|             </p> | ||||
|             <p *ngIf="access.canviewallsubmissions && !showGrade(assessment.gradinggradeover) && showGrade(assessment.gradinggrade)"> | ||||
|                 {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}} | ||||
|             </p> | ||||
|             <p *ngIf="access.canviewallsubmissions && showGrade(assessment.gradinggradeover)" class="core-overriden-grade"> | ||||
|                 {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggradeover}} | ||||
|             </p> | ||||
|             <p *ngIf="assessment.weight && assessment.weight != 1"> | ||||
|                 {{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }} | ||||
|             </p> | ||||
|             <ion-badge *ngIf="!assessment.grade" color="danger">{{ 'addon.mod_workshop.notassessed' | translate }}</ion-badge> | ||||
|             <ion-button expand="block" *ngIf="canSelfAssess && !showGrade(assessment.grade)" (click)="gotoOwnAssessment($event)"> | ||||
|                 {{ 'addon.mod_workshop.assess' | translate }} | ||||
|             </ion-button> | ||||
|         </ion-label> | ||||
|         <ion-note slot="end" *ngIf="offline"> | ||||
|             <ion-icon name="fas-clock"></ion-icon>{{ 'core.notsent' | translate }} | ||||
|         </ion-note> | ||||
|     </ion-item> | ||||
| </core-loading> | ||||
							
								
								
									
										170
									
								
								src/addons/mod/workshop/components/assessment/assessment.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/addons/mod/workshop/components/assessment/assessment.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
| import { Params } from '@angular/router'; | ||||
| import { CoreCourseModule } from '@features/course/services/course-helper'; | ||||
| import { CoreUser, CoreUserProfile } from '@features/user/services/user'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { AddonModWorkshopData, AddonModWorkshopGetWorkshopAccessInformationWSResponse } from '../../services/workshop'; | ||||
| import { | ||||
|     AddonModWorkshopHelper, | ||||
|     AddonModWorkshopSubmissionAssessmentWithFormData, | ||||
|     AddonModWorkshopSubmissionDataWithOfflineData, | ||||
| } from '../../services/workshop-helper'; | ||||
| import { AddonModWorkshopOffline } from '../../services/workshop-offline'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays workshop assessment. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-workshop-assessment', | ||||
|     templateUrl: 'addon-mod-workshop-assessment.html', | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentComponent implements OnInit { | ||||
| 
 | ||||
|     @Input() assessment!: AddonModWorkshopSubmissionAssessmentWithFormData; | ||||
|     @Input() courseId!: number; | ||||
|     @Input() workshop!: AddonModWorkshopData; | ||||
|     @Input() access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse; | ||||
|     @Input() protected submission!: AddonModWorkshopSubmissionDataWithOfflineData; | ||||
|     @Input() protected module!: CoreCourseModule; | ||||
| 
 | ||||
|     canViewAssessment = false; | ||||
|     canSelfAssess = false; | ||||
|     profile?: CoreUserProfile; | ||||
|     showGrade: (grade?: string | number) => boolean; | ||||
|     offline = false; | ||||
|     loaded = false; | ||||
| 
 | ||||
|     protected currentUserId: number; | ||||
|     protected assessmentId?: number; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.currentUserId = CoreSites.getCurrentSiteUserId(); | ||||
|         this.showGrade = AddonModWorkshopHelper.showGrade; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         const canAssess = this.access && this.access.assessingallowed; | ||||
|         const userId = this.assessment.reviewerid; | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         this.assessmentId = this.assessment.id; | ||||
|         this.canViewAssessment = !!this.assessment.grade; | ||||
|         this.canSelfAssess = canAssess && userId == this.currentUserId; | ||||
| 
 | ||||
|         if (userId) { | ||||
|             promises.push(CoreUser.getProfile(userId, this.courseId, true).then((profile) => { | ||||
|                 this.profile = profile; | ||||
| 
 | ||||
|                 return; | ||||
|             })); | ||||
|         } | ||||
| 
 | ||||
|         let assessOffline: Promise<void>; | ||||
|         if (userId == this.currentUserId) { | ||||
|             assessOffline = AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId) .then((offlineAssess) => { | ||||
|                 this.offline = true; | ||||
|                 this.assessment.weight = <number>offlineAssess.inputdata.weight; | ||||
| 
 | ||||
|                 return; | ||||
|             }); | ||||
|         } else { | ||||
|             assessOffline = AddonModWorkshopOffline.getEvaluateAssessment(this.workshop.id, this.assessmentId) | ||||
|                 .then((offlineAssess) => { | ||||
|                     this.offline = true; | ||||
|                     this.assessment.gradinggradeover = offlineAssess.gradinggradeover; | ||||
|                     this.assessment.weight = <number>offlineAssess.weight; | ||||
| 
 | ||||
|                     return; | ||||
| 
 | ||||
|                 }); | ||||
|         } | ||||
| 
 | ||||
|         promises.push(assessOffline.catch(() => { | ||||
|             this.offline = false; | ||||
|             // Ignore errors.
 | ||||
|         })); | ||||
| 
 | ||||
|         Promise.all(promises).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Navigate to the assessment. | ||||
|      */ | ||||
|     async gotoAssessment(event: Event): Promise<void> { | ||||
|         if (!this.canSelfAssess && this.canViewAssessment) { | ||||
|             event.preventDefault(); | ||||
|             event.stopPropagation(); | ||||
| 
 | ||||
|             const params: Params = { | ||||
|                 assessment: this.assessment, | ||||
|                 submission: this.submission, | ||||
|                 profile: this.profile, | ||||
|             }; | ||||
| 
 | ||||
|             if (!this.submission) { | ||||
|                 const modal = await CoreDomUtils.showModalLoading(); | ||||
| 
 | ||||
|                 try { | ||||
|                     params.submission = await AddonModWorkshopHelper.getSubmissionById( | ||||
|                         this.workshop.id, | ||||
|                         this.assessment.submissionid, | ||||
|                         { cmId: this.workshop.coursemodule }, | ||||
|                     ); | ||||
| 
 | ||||
|                     CoreNavigator.navigate(String(this.assessmentId), { params }); | ||||
|                 } catch (error) { | ||||
|                     CoreDomUtils.showErrorModalDefault(error, 'Cannot load submission'); | ||||
|                 } finally { | ||||
|                     modal.dismiss(); | ||||
|                 } | ||||
|             } else { | ||||
|                 CoreNavigator.navigate(String(this.assessmentId), { params }); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Navigate to my own assessment. | ||||
|      */ | ||||
|     gotoOwnAssessment(event: Event): void { | ||||
|         if (!this.canSelfAssess) { | ||||
|             return; | ||||
|         } | ||||
|         event.preventDefault(); | ||||
|         event.stopPropagation(); | ||||
| 
 | ||||
|         const params: Params = { | ||||
|             module: this.module, | ||||
|             workshop: this.workshop, | ||||
|             access: this.access, | ||||
|             profile: this.profile, | ||||
|             submission: this.submission, | ||||
|             assessment: this.assessment, | ||||
|         }; | ||||
| 
 | ||||
|         CoreNavigator.navigate(String(this.submission.id), params); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										107
									
								
								src/addons/mod/workshop/pages/assessment/assessment.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/addons/mod/workshop/pages/assessment/assessment.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| <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]="workshop && workshop.coursemodule" | ||||
|                 [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end" [hidden]="!evaluating"> | ||||
|             <ion-button fill="clear" (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate"> | ||||
|                 {{ 'core.save' | translate }} | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshAssessment($event.target)" *ngIf="!evaluating"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
| 
 | ||||
|         <ion-item class="ion-text-wrap"> | ||||
|             <core-user-avatar *ngIf="profile" [user]="profile" slot="start" [courseId]="courseId" [userId]="profile.id"> | ||||
|             </core-user-avatar> | ||||
| 
 | ||||
|             <ion-label> | ||||
|                 <h2 *ngIf="profile && profile.fullname">{{profile.fullname}}</h2> | ||||
| 
 | ||||
|                 <p *ngIf="workshop && assessment && showGrade(assessment.grade)"> | ||||
|                     {{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}} | ||||
|                 </p> | ||||
|                 <p *ngIf="workshop && access && access.canviewallsubmissions && assessment && showGrade(assessment.gradinggrade)" | ||||
|                     [class.core-has-overriden-grade]=" showGrade(assessment.gradinggrade)"> | ||||
|                     {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}} | ||||
|                 </p> | ||||
|                 <p *ngIf="access && access.canviewallsubmissions && assessment && showGrade(assessment.gradinggradeover)" | ||||
|                     class="core-overriden-grade"> | ||||
|                     {{ 'addon.mod_workshop.gradinggradeover' | translate }}: {{assessment.gradinggradeover}} | ||||
|                 </p> | ||||
|                 <p *ngIf="assessment && assessment.weight && assessment.weight != 1"> | ||||
|                     {{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }} | ||||
|                 </p> | ||||
|                 <ion-badge *ngIf="!assessment || !showGrade(assessment.grade)" color="danger"> | ||||
|                     {{ 'addon.mod_workshop.notassessed' | translate }} | ||||
|                 </ion-badge> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <addon-mod-workshop-assessment-strategy | ||||
|             *ngIf="assessment && assessmentId && showGrade(assessment.grade) && workshop && access" [workshop]="workshop" | ||||
|             [access]="access" [assessmentId]="assessmentId" [userId]="profile && profile.id" [strategy]="strategy"> | ||||
|         </addon-mod-workshop-assessment-strategy> | ||||
| 
 | ||||
|         <form ion-list [formGroup]="evaluateForm" *ngIf="evaluating" #evaluateFormEl> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label><h2>{{ 'addon.mod_workshop.assessmentsettings' | translate }}</h2></ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="access?.canallocate"> | ||||
|                 <ion-label position="stacked"> | ||||
|                     <span core-mark-required="true"> | ||||
|                         {{ 'addon.mod_workshop.assessmentweight' | translate }} | ||||
|                     </span> | ||||
|                 </ion-label> | ||||
|                 <ion-select formControlName="weight" required="true" interface="action-sheet"> | ||||
|                     <ion-select-option *ngFor="let w of weights" [value]="w">{{ w }}</ion-select-option> | ||||
|                 </ion-select> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_workshop.gradinggradecalculated' | translate }}</h2> | ||||
|                     <p>{{ assessment.gradinggrade }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="access?.canoverridegrades"> | ||||
|                 <ion-label position="stacked">{{ 'addon.mod_workshop.gradinggradeover' | translate }}</ion-label> | ||||
|                 <ion-select formControlName="grade" interface="action-sheet"> | ||||
|                     <ion-select-option *ngFor="let grade of evaluationGrades" [value]="grade.value"> | ||||
|                         {{grade.label}} | ||||
|                     </ion-select-option> | ||||
|                 </ion-select> | ||||
|             </ion-item> | ||||
|             <ion-item *ngIf="access?.canoverridegrades"> | ||||
|                 <ion-label position="stacked">{{ 'addon.mod_workshop.feedbackreviewer' | translate }}</ion-label> | ||||
|                 <core-rich-text-editor [control]="evaluateForm.controls['text']" name="text" | ||||
|                     [autoSave]="true" contextLevel="module" [contextInstanceId]="workshop?.coursemodule" | ||||
|                     elementId="feedbackreviewer_editor" [draftExtraParams]="{asid: assessmentId}"> | ||||
|                 </core-rich-text-editor> | ||||
|             </ion-item> | ||||
|         </form> | ||||
|         <ion-list *ngIf="!evaluating && evaluate && evaluate.text"> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <core-user-avatar *ngIf="evaluateByProfile" [user]="evaluateByProfile" slot="start" | ||||
|                     [courseId]="courseId" [userId]="evaluateByProfile.id"></core-user-avatar> | ||||
|                 <ion-label> | ||||
|                     <h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname"> | ||||
|                         {{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }} | ||||
|                     </h2> | ||||
|                     <core-format-text [text]="evaluate.text" contextLevel="module" [contextInstanceId]="workshop?.coursemodule" | ||||
|                         [courseId]="courseId"> | ||||
|                     </core-format-text> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										398
									
								
								src/addons/mod/workshop/pages/assessment/assessment.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								src/addons/mod/workshop/pages/assessment/assessment.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,398 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; | ||||
| import { FormGroup, FormBuilder, Validators } from '@angular/forms'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { CoreGradesHelper, CoreGradesMenuItem } from '@features/grades/services/grades-helper'; | ||||
| import { CoreUser, CoreUserProfile } from '@features/user/services/user'; | ||||
| import { CanLeave } from '@guards/can-leave'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreForms } from '@singletons/form'; | ||||
| import { | ||||
|     AddonModWorkshop, | ||||
|     AddonModWorkshopAssessmentSavedChangedEventData, | ||||
|     AddonModWorkshopData, | ||||
|     AddonModWorkshopGetWorkshopAccessInformationWSResponse, | ||||
|     AddonModWorkshopPhase, | ||||
|     AddonModWorkshopProvider, | ||||
|     AddonModWorkshopSubmissionData, | ||||
| } from '../../services/workshop'; | ||||
| import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper'; | ||||
| import { AddonModWorkshopOffline } from '../../services/workshop-offline'; | ||||
| import { AddonModWorkshopSyncProvider } from '../../services/workshop-sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays a workshop assessment. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-workshop-assessment-page', | ||||
|     templateUrl: 'assessment.html', | ||||
| }) | ||||
| export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLeave { | ||||
| 
 | ||||
|     @ViewChild('evaluateFormEl') formElement!: ElementRef; | ||||
| 
 | ||||
|     assessment!: AddonModWorkshopSubmissionAssessmentWithFormData; | ||||
|     submission!: AddonModWorkshopSubmissionData; | ||||
|     profile!: CoreUserProfile; | ||||
|     courseId!: number; | ||||
|     access?: AddonModWorkshopGetWorkshopAccessInformationWSResponse; | ||||
|     assessmentId!: number; | ||||
|     evaluating = false; | ||||
|     loaded = false; | ||||
|     showGrade: (grade?: string | number) => boolean; | ||||
|     evaluateForm: FormGroup; | ||||
|     maxGrade?: number; | ||||
|     workshop?: AddonModWorkshopData; | ||||
|     strategy?: string; | ||||
|     title = ''; | ||||
|     evaluate: AddonModWorkshopAssessmentEvaluation = { | ||||
|         text: '', | ||||
|         grade: -1, | ||||
|         weight: 1, | ||||
|     }; | ||||
| 
 | ||||
|     weights: number[] = []; | ||||
|     evaluateByProfile?: CoreUserProfile; | ||||
|     evaluationGrades: CoreGradesMenuItem[] =[]; | ||||
| 
 | ||||
|     protected workshopId!: number; | ||||
|     protected originalEvaluation: AddonModWorkshopAssessmentEvaluation = { | ||||
|         text: '', | ||||
|         grade: -1, | ||||
|         weight: 1, | ||||
|     }; | ||||
| 
 | ||||
|     protected hasOffline = false; | ||||
|     protected syncObserver: CoreEventObserver; | ||||
|     protected isDestroyed = false; | ||||
|     protected siteId: string; | ||||
|     protected currentUserId: number; | ||||
|     protected forceLeave = false; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected fb: FormBuilder, | ||||
|     ) { | ||||
|         this.siteId = CoreSites.getCurrentSiteId(); | ||||
|         this.currentUserId = CoreSites.getCurrentSiteUserId(); | ||||
| 
 | ||||
|         this.showGrade = AddonModWorkshopHelper.showGrade; | ||||
| 
 | ||||
|         this.evaluateForm = new FormGroup({}); | ||||
|         this.evaluateForm.addControl('weight', this.fb.control('', Validators.required)); | ||||
|         this.evaluateForm.addControl('grade', this.fb.control('')); | ||||
|         this.evaluateForm.addControl('text', this.fb.control('')); | ||||
| 
 | ||||
|         // Refresh workshop on sync.
 | ||||
|         this.syncObserver = CoreEvents.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => { | ||||
|             // Update just when all database is synced.
 | ||||
|             if (this.workshopId === data.workshopId) { | ||||
|                 this.loaded = false; | ||||
|                 this.refreshAllData(); | ||||
|             } | ||||
|         }, this.siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.assessment = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionAssessmentWithFormData>('assessment')!; | ||||
|         this.submission = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionData>('submission')!; | ||||
|         this.profile = CoreNavigator.getRouteParam<CoreUserProfile>('profile')!; | ||||
|         this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; | ||||
| 
 | ||||
|         this.assessmentId = this.assessment.id; | ||||
|         this.workshopId = this.submission.workshopid; | ||||
| 
 | ||||
|         this.fetchAssessmentData(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we can leave the page or not. | ||||
|      * | ||||
|      * @return Resolved if we can leave it, rejected if not. | ||||
|      */ | ||||
|     async canLeave(): Promise<boolean> { | ||||
|         if (this.forceLeave || !this.evaluating) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (!this.hasEvaluationChanged()) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // Show confirmation if some data has been modified.
 | ||||
|         await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); | ||||
| 
 | ||||
|         CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the assessment data. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async fetchAssessmentData(): Promise<void> { | ||||
|         try { | ||||
|             this.workshop = await AddonModWorkshop.getWorkshopById(this.courseId, this.workshopId); | ||||
|             this.title = this.workshop.name; | ||||
|             this.strategy = this.workshop.strategy; | ||||
| 
 | ||||
|             const gradeInfo = await CoreCourse.getModuleBasicGradeInfo(this.workshop.coursemodule); | ||||
|             this.maxGrade = gradeInfo?.grade; | ||||
| 
 | ||||
|             this.access = await AddonModWorkshop.getWorkshopAccessInformation( | ||||
|                 this.workshopId, | ||||
|                 { cmId: this.workshop.coursemodule }, | ||||
|             ); | ||||
| 
 | ||||
|             // Load Weights selector.
 | ||||
|             if (this.assessmentId && (this.access.canallocate || this.access.canoverridegrades)) { | ||||
|                 if (!this.isDestroyed) { | ||||
|                     // Block the workshop.
 | ||||
|                     CoreSync.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId); | ||||
|                 } | ||||
| 
 | ||||
|                 this.evaluating = true; | ||||
|             } else { | ||||
|                 this.evaluating = false; | ||||
|             } | ||||
| 
 | ||||
|             if (!this.evaluating && this.workshop.phase != AddonModWorkshopPhase.PHASE_CLOSED) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Get all info of the assessment.
 | ||||
|             const assessment = await AddonModWorkshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, { | ||||
|                 userId: this.profile && this.profile.id, | ||||
|                 cmId: this.workshop.coursemodule, | ||||
|             }); | ||||
| 
 | ||||
|             this.assessment = AddonModWorkshopHelper.realGradeValue(this.workshop, assessment); | ||||
|             this.evaluate.text = this.assessment.feedbackreviewer || ''; | ||||
|             this.evaluate.weight = this.assessment.weight; | ||||
| 
 | ||||
|             if (this.evaluating) { | ||||
|                 if (this.access.canallocate) { | ||||
|                     this.weights = []; | ||||
|                     for (let i = 16; i >= 0; i--) { | ||||
|                         this.weights[i] = i; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (this.access.canoverridegrades) { | ||||
|                     const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden'); | ||||
|                     this.evaluationGrades = | ||||
|                         await CoreGradesHelper.makeGradesMenu(this.workshop.gradinggrade, undefined, defaultGrade, -1); | ||||
|                 } | ||||
| 
 | ||||
|                 try { | ||||
|                     const offlineAssess = await AddonModWorkshopOffline.getEvaluateAssessment(this.workshopId, this.assessmentId); | ||||
|                     this.hasOffline = true; | ||||
|                     this.evaluate.weight = offlineAssess.weight; | ||||
|                     if (this.access.canoverridegrades) { | ||||
|                         this.evaluate.text = offlineAssess.feedbacktext || ''; | ||||
|                         this.evaluate.grade = parseInt(offlineAssess.gradinggradeover, 10) || -1; | ||||
|                     } | ||||
|                 } catch { | ||||
|                     this.hasOffline = false; | ||||
|                     // No offline, load online.
 | ||||
|                     if (this.access.canoverridegrades) { | ||||
|                         this.evaluate.text = this.assessment.feedbackreviewer || ''; | ||||
|                         this.evaluate.grade = parseInt(String(this.assessment.gradinggradeover), 10) || -1; | ||||
|                     } | ||||
|                 } finally { | ||||
|                     this.originalEvaluation.weight = this.evaluate.weight; | ||||
|                     if (this.access.canoverridegrades) { | ||||
|                         this.originalEvaluation.text = this.evaluate.text; | ||||
|                         this.originalEvaluation.grade = this.evaluate.grade; | ||||
|                     } | ||||
| 
 | ||||
|                     this.evaluateForm.controls['weight'].setValue(this.evaluate.weight); | ||||
|                     if (this.access.canoverridegrades) { | ||||
|                         this.evaluateForm.controls['grade'].setValue(this.evaluate.grade); | ||||
|                         this.evaluateForm.controls['text'].setValue(this.evaluate.text); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|             } else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.assessment.gradinggradeoverby) { | ||||
|                 this.evaluateByProfile = await CoreUser.getProfile(this.assessment.gradinggradeoverby, this.courseId, true); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'mm.course.errorgetmodule', true); | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Force leaving the page, without checking for changes. | ||||
|      */ | ||||
|     protected forceLeavePage(): void { | ||||
|         this.forceLeave = true; | ||||
|         CoreNavigator.back(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if data has changed. | ||||
|      * | ||||
|      * @return True if changed, false otherwise. | ||||
|      */ | ||||
|     protected hasEvaluationChanged(): boolean { | ||||
|         if (!this.loaded || !this.evaluating) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const inputData = this.evaluateForm.value; | ||||
| 
 | ||||
|         if (this.originalEvaluation.weight != inputData.weight) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (this.access && this.access.canoverridegrades) { | ||||
|             if (this.originalEvaluation.text != inputData.text) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             if (this.originalEvaluation.grade != inputData.grade) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to refresh all the data. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async refreshAllData(): Promise<void> { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(AddonModWorkshop.invalidateWorkshopData(this.courseId)); | ||||
|         promises.push(AddonModWorkshop.invalidateWorkshopAccessInformationData(this.workshopId)); | ||||
|         promises.push(AddonModWorkshop.invalidateReviewerAssesmentsData(this.workshopId)); | ||||
| 
 | ||||
|         if (this.assessmentId) { | ||||
|             promises.push(AddonModWorkshop.invalidateAssessmentFormData(this.workshopId, this.assessmentId)); | ||||
|             promises.push(AddonModWorkshop.invalidateAssessmentData(this.workshopId, this.assessmentId)); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await Promise.all(promises); | ||||
|         } finally { | ||||
|             CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, null, this.siteId); | ||||
| 
 | ||||
|             await this.fetchAssessmentData(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Pull to refresh. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      */ | ||||
|     refreshAssessment(refresher: IonRefresher): void { | ||||
|         if (this.loaded) { | ||||
|             this.refreshAllData().finally(() => { | ||||
|                 refresher?.complete(); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the assessment evaluation. | ||||
|      */ | ||||
|     async saveEvaluation(): Promise<void> { | ||||
|         // Check if data has changed.
 | ||||
|         if (this.hasEvaluationChanged()) { | ||||
|             await this.sendEvaluation(); | ||||
|         } | ||||
| 
 | ||||
|         // Go back.
 | ||||
|         this.forceLeavePage(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sends the evaluation to be saved on the server. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async sendEvaluation(): Promise<void> { | ||||
|         const modal = await CoreDomUtils.showModalLoading('core.sending', true); | ||||
|         const inputData: AddonModWorkshopAssessmentEvaluation = this.evaluateForm.value; | ||||
| 
 | ||||
|         const grade = inputData.grade >= 0 ? String(inputData.grade) : ''; | ||||
|         // Add some HTML to the message if needed.
 | ||||
|         const text = CoreTextUtils.formatHtmlLines(inputData.text); | ||||
| 
 | ||||
|         try { | ||||
|             // Try to send it to server.
 | ||||
|             const result = await AddonModWorkshop.evaluateAssessment( | ||||
|                 this.workshopId, | ||||
|                 this.assessmentId, | ||||
|                 this.courseId, | ||||
|                 text, | ||||
|                 inputData.weight, | ||||
|                 grade, | ||||
|             ); | ||||
| 
 | ||||
|             CoreForms.triggerFormSubmittedEvent(this.formElement, !!result, this.siteId); | ||||
| 
 | ||||
|             const data: AddonModWorkshopAssessmentSavedChangedEventData = { | ||||
|                 workshopId: this.workshopId, | ||||
|                 assessmentId: this.assessmentId, | ||||
|                 userId: this.currentUserId, | ||||
|             }; | ||||
| 
 | ||||
|             return AddonModWorkshop.invalidateAssessmentData(this.workshopId, this.assessmentId).finally(() => { | ||||
|                 CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, data, this.siteId); | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Cannot save assessment evaluation'); | ||||
|         } finally { | ||||
|             modal.dismiss(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
| 
 | ||||
|         this.syncObserver?.off(); | ||||
|         // Restore original back functions.
 | ||||
|         CoreSync.unblockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type AddonModWorkshopAssessmentEvaluation = { | ||||
|     text: string; | ||||
|     grade: number; | ||||
|     weight: number; | ||||
| }; | ||||
| @ -0,0 +1,46 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title>{{ 'addon.mod_workshop.editsubmission' | translate }}</ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="save()" [attr.aria-label]="'core.save' | translate"> | ||||
|                 {{ 'core.save' | translate }} | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <form ion-list [formGroup]="editForm" *ngIf="workshop" #editFormEl> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label position="stacked"> | ||||
|                     <span core-mark-required="true"> | ||||
|                         {{ 'addon.mod_workshop.submissiontitle' | translate }} | ||||
|                     </span> | ||||
|                 </ion-label> | ||||
|                 <ion-input name="title" type="text" [placeholder]="'addon.mod_workshop.submissiontitle' | translate" | ||||
|                     formControlName="title"> | ||||
|                 </ion-input> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <ion-item *ngIf="textAvailable"> | ||||
|                 <ion-label position="stacked"> | ||||
|                     <span [core-mark-required]="textRequired"> | ||||
|                         {{ 'addon.mod_workshop.submissioncontent' | translate }} | ||||
|                     </span> | ||||
|                 </ion-label> | ||||
|                 <core-rich-text-editor [control]="editForm.controls['content']" name="content" | ||||
|                     [placeholder]="'addon.mod_workshop.submissioncontent' | translate"  name="content" [component]="component" | ||||
|                     [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id" | ||||
|                     elementId="content_editor" [draftExtraParams]="editorExtraParams"></core-rich-text-editor> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <core-attachments *ngIf="fileAvailable" [files]="submission?.attachmentfiles || []" [maxSize]="workshop.maxbytes" | ||||
|                 [maxSubmissions]="workshop.nattachments" [component]="component" [componentId]="workshop.coursemodule" | ||||
|                 allowOffline="true" [acceptedTypes]="workshop.submissionfiletypes" [required]="fileRequired"> | ||||
|             </core-attachments> | ||||
|         </form> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										476
									
								
								src/addons/mod/workshop/pages/edit-submission/edit-submission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										476
									
								
								src/addons/mod/workshop/pages/edit-submission/edit-submission.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,476 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; | ||||
| import { FormGroup, FormBuilder, Validators } from '@angular/forms'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreCourseModule } from '@features/course/services/course-helper'; | ||||
| import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; | ||||
| import { CanLeave } from '@guards/can-leave'; | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; | ||||
| import { CoreFileSession } from '@services/file-session'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { CoreForms } from '@singletons/form'; | ||||
| import { | ||||
|     AddonModWorkshopProvider, | ||||
|     AddonModWorkshop, | ||||
|     AddonModWorkshopSubmissionType, | ||||
|     AddonModWorkshopSubmissionChangedEventData, | ||||
|     AddonModWorkshopAction, | ||||
|     AddonModWorkshopGetWorkshopAccessInformationWSResponse, | ||||
|     AddonModWorkshopData, | ||||
| } from '../../services/workshop'; | ||||
| import { AddonModWorkshopHelper, AddonModWorkshopSubmissionDataWithOfflineData } from '../../services/workshop-helper'; | ||||
| import { AddonModWorkshopOffline } from '../../services/workshop-offline'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the workshop edit submission. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-workshop-edit-submission', | ||||
|     templateUrl: 'edit-submission.html', | ||||
| }) | ||||
| export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, CanLeave { | ||||
| 
 | ||||
|     @ViewChild('editFormEl') formElement!: ElementRef; | ||||
| 
 | ||||
|     module!: CoreCourseModule; | ||||
|     courseId!: number; | ||||
|     access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse; | ||||
|     submission?: AddonModWorkshopSubmissionDataWithOfflineData; | ||||
| 
 | ||||
|     loaded = false; | ||||
|     component = AddonModWorkshopProvider.COMPONENT; | ||||
|     componentId!: number; | ||||
|     editForm: FormGroup; // The form group.
 | ||||
|     editorExtraParams:  Record<string, unknown> = {}; // Extra params to identify the draft.
 | ||||
|     workshop?: AddonModWorkshopData; | ||||
|     textAvailable = false; | ||||
|     textRequired = false; | ||||
|     fileAvailable = false; | ||||
|     fileRequired = false; | ||||
| 
 | ||||
|     protected workshopId!: number; | ||||
|     protected submissionId = 0; | ||||
|     protected userId: number; | ||||
|     protected originalData: AddonModWorkshopEditSubmissionInputData = { | ||||
|         title: '', | ||||
|         content: '', | ||||
|         attachmentfiles: [], | ||||
|     }; | ||||
| 
 | ||||
|     protected hasOffline = false; | ||||
|     protected editing = false; | ||||
|     protected forceLeave = false; | ||||
|     protected siteId: string; | ||||
|     protected isDestroyed = false; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected fb: FormBuilder, | ||||
|     ) { | ||||
| 
 | ||||
|         this.userId = CoreSites.getCurrentSiteUserId(); | ||||
|         this.siteId = CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         this.editForm = new FormGroup({}); | ||||
|         this.editForm.addControl('title', this.fb.control('', Validators.required)); | ||||
|         this.editForm.addControl('content', this.fb.control('')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!; | ||||
|         this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; | ||||
|         this.access = CoreNavigator.getRouteParam<AddonModWorkshopGetWorkshopAccessInformationWSResponse>('access')!; | ||||
|         this.submissionId = CoreNavigator.getRouteNumberParam('submissionId') || 0; | ||||
| 
 | ||||
|         if (this.submissionId > 0) { | ||||
|             this.editorExtraParams.id = this.submissionId; | ||||
|         } | ||||
| 
 | ||||
|         this.workshopId = this.module.instance!; | ||||
|         this.componentId = this.module.id; | ||||
| 
 | ||||
|         if (!this.isDestroyed) { | ||||
|             // Block the workshop.
 | ||||
|             CoreSync.blockOperation(this.component, this.workshopId); | ||||
|         } | ||||
| 
 | ||||
|         this.fetchSubmissionData(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we can leave the page or not. | ||||
|      * | ||||
|      * @return Resolved if we can leave it, rejected if not. | ||||
|      */ | ||||
|     async canLeave(): Promise<boolean> { | ||||
|         if (this.forceLeave) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // Check if data has changed.
 | ||||
|         if (this.hasDataChanged()) { | ||||
|             // Show confirmation if some data has been modified.
 | ||||
|             await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); | ||||
|         } | ||||
| 
 | ||||
|         if (this.submission?.attachmentfiles) { | ||||
|             // Delete the local files from the tmp folder.
 | ||||
|             CoreFileUploader.clearTmpFiles(this.submission.attachmentfiles); | ||||
|         } | ||||
| 
 | ||||
|         CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the submission data. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async fetchSubmissionData(): Promise<void> { | ||||
|         try { | ||||
|             this.workshop = await AddonModWorkshop.getWorkshop(this.courseId, this.module.id); | ||||
|             this.textAvailable = (this.workshop.submissiontypetext != AddonModWorkshopSubmissionType.SUBMISSION_TYPE_DISABLED); | ||||
|             this.textRequired = (this.workshop.submissiontypetext == AddonModWorkshopSubmissionType.SUBMISSION_TYPE_REQUIRED); | ||||
|             this.fileAvailable = (this.workshop.submissiontypefile != AddonModWorkshopSubmissionType.SUBMISSION_TYPE_DISABLED); | ||||
|             this.fileRequired = (this.workshop.submissiontypefile == AddonModWorkshopSubmissionType.SUBMISSION_TYPE_REQUIRED); | ||||
| 
 | ||||
|             this.editForm.controls.content.setValidators(this.textRequired ? Validators.required : null); | ||||
| 
 | ||||
|             if (this.submissionId > 0) { | ||||
|                 this.editing = true; | ||||
| 
 | ||||
|                 this.submission = | ||||
|                     await AddonModWorkshopHelper.getSubmissionById(this.workshopId, this.submissionId, { cmId: this.module.id }); | ||||
| 
 | ||||
|                 const canEdit = this.userId == this.submission.authorid && | ||||
|                     this.access.cansubmit && | ||||
|                     this.access.modifyingsubmissionallowed; | ||||
| 
 | ||||
|                 if (!canEdit) { | ||||
|                     // Should not happen, but go back if does.
 | ||||
|                     this.forceLeavePage(); | ||||
| 
 | ||||
|                     return; | ||||
|                 } | ||||
|             } else if (!this.access.cansubmit || !this.access.creatingsubmissionallowed) { | ||||
|                 // Should not happen, but go back if does.
 | ||||
|                 this.forceLeavePage(); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const submissionsActions = await AddonModWorkshopOffline.getSubmissions(this.workshopId); | ||||
|             if (submissionsActions && submissionsActions.length) { | ||||
|                 this.hasOffline = true; | ||||
|                 this.submission = await AddonModWorkshopHelper.applyOfflineData(this.submission, submissionsActions); | ||||
|             } else { | ||||
|                 this.hasOffline = false; | ||||
|             } | ||||
| 
 | ||||
|             if (this.submission) { | ||||
|                 this.originalData.title = this.submission.title || ''; | ||||
|                 this.originalData.content = this.submission.content || ''; | ||||
|                 this.originalData.attachmentfiles = []; | ||||
| 
 | ||||
|                 (this.submission.attachmentfiles || []).forEach((file) => { | ||||
|                     let filename = CoreFile.getFileName(file); | ||||
|                     if (!filename) { | ||||
|                         // We don't have filename, extract it from the path.
 | ||||
|                         filename = CoreFileHelper.getFilenameFromPath(file) || ''; | ||||
|                     } | ||||
| 
 | ||||
|                     this.originalData.attachmentfiles.push({ | ||||
|                         filename, | ||||
|                         fileurl: 'fileurl' in file ? file.fileurl : '', | ||||
|                     }); | ||||
|                 }); | ||||
| 
 | ||||
|                 this.editForm.controls['title'].setValue(this.submission.title); | ||||
|                 this.editForm.controls['content'].setValue(this.submission.content); | ||||
|             } | ||||
| 
 | ||||
|             CoreFileSession.setFiles( | ||||
|                 this.component, | ||||
|                 this.getFilesComponentId(), | ||||
|                 this.submission?.attachmentfiles || [], | ||||
|             ); | ||||
| 
 | ||||
|             this.loaded = true; | ||||
|         } catch (error) { | ||||
|             this.loaded = false; | ||||
| 
 | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); | ||||
| 
 | ||||
|             this.forceLeavePage(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Force leaving the page, without checking for changes. | ||||
|      */ | ||||
|     protected forceLeavePage(): void { | ||||
|         this.forceLeave = true; | ||||
|         CoreNavigator.back(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the form input data. | ||||
|      * | ||||
|      * @return Object with all the info. | ||||
|      */ | ||||
|     protected getInputData(): AddonModWorkshopEditSubmissionInputData { | ||||
|         const values: AddonModWorkshopEditSubmissionInputData = { | ||||
|             title: this.editForm.value.title, | ||||
|             content: '', | ||||
|             attachmentfiles: [], | ||||
|         }; | ||||
| 
 | ||||
|         if (this.textAvailable) { | ||||
|             values.content = this.editForm.value.content || ''; | ||||
|         } | ||||
| 
 | ||||
|         if (this.fileAvailable) { | ||||
|             values.attachmentfiles = CoreFileSession.getFiles(this.component, this.getFilesComponentId()) || []; | ||||
|         } | ||||
| 
 | ||||
|         return values; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if data has changed. | ||||
|      * | ||||
|      * @return True if changed or false if not. | ||||
|      */ | ||||
|     protected hasDataChanged(): boolean { | ||||
|         if (!this.loaded) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const inputData = this.getInputData(); | ||||
|         if (this.originalData.title != inputData.title || this.textAvailable && this.originalData.content != inputData.content) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (this.fileAvailable) { | ||||
|             return CoreFileUploader.areFileListDifferent(inputData.attachmentfiles, this.originalData.attachmentfiles); | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the submission. | ||||
|      */ | ||||
|     async save(): Promise<void> { | ||||
|         // Check if data has changed.
 | ||||
|         if (this.hasDataChanged()) { | ||||
|             try { | ||||
|                 await this.saveSubmission(); | ||||
|                 // Go back to entry list.
 | ||||
|                 this.forceLeavePage(); | ||||
|             } catch{ | ||||
|                 // Nothing to do.
 | ||||
|             } | ||||
|         } else { | ||||
|             // Nothing to save, just go back.
 | ||||
|             this.forceLeavePage(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Send submission and save. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async saveSubmission(): Promise<void> { | ||||
|         const inputData = this.getInputData(); | ||||
| 
 | ||||
|         if (!inputData.title) { | ||||
|             CoreDomUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredtitle'); | ||||
| 
 | ||||
|             throw new CoreError(Translate.instant('addon.mod_workshop.submissionrequiredtitle')); | ||||
|         } | ||||
| 
 | ||||
|         const noText = CoreTextUtils.htmlIsBlank(inputData.content); | ||||
|         const noFiles = !inputData.attachmentfiles.length; | ||||
| 
 | ||||
|         if ((this.textRequired && noText) || (this.fileRequired && noFiles) || (noText && noFiles)) { | ||||
|             CoreDomUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredcontent'); | ||||
| 
 | ||||
|             throw new CoreError(Translate.instant('addon.mod_workshop.submissionrequiredcontent')); | ||||
|         } | ||||
| 
 | ||||
|         let saveOffline = false; | ||||
| 
 | ||||
|         const modal = await CoreDomUtils.showModalLoading('core.sending', true); | ||||
|         const submissionId = this.submission?.id; | ||||
| 
 | ||||
|         // Add some HTML to the message if needed.
 | ||||
|         if (this.textAvailable) { | ||||
|             inputData.content = CoreTextUtils.formatHtmlLines(inputData.content); | ||||
|         } | ||||
| 
 | ||||
|         // Upload attachments first if any.
 | ||||
|         let allowOffline = !inputData.attachmentfiles.length; | ||||
|         try { | ||||
|             let attachmentsId: CoreFileUploaderStoreFilesResult | number | undefined; | ||||
|             try { | ||||
|                 attachmentsId = await AddonModWorkshopHelper.uploadOrStoreSubmissionFiles( | ||||
|                     this.workshopId, | ||||
|                     inputData.attachmentfiles, | ||||
|                     false, | ||||
|                 ); | ||||
|             } catch { | ||||
|                 // Cannot upload them in online, save them in offline.
 | ||||
|                 saveOffline = true; | ||||
|                 allowOffline = true; | ||||
| 
 | ||||
|                 attachmentsId = await AddonModWorkshopHelper.uploadOrStoreSubmissionFiles( | ||||
|                     this.workshopId, | ||||
|                     inputData.attachmentfiles, | ||||
|                     true, | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             if (!saveOffline && !this.fileAvailable) { | ||||
|                 attachmentsId = undefined; | ||||
|             } | ||||
| 
 | ||||
|             let newSubmissionId: number | false; | ||||
|             if (this.editing) { | ||||
|                 if (saveOffline) { | ||||
|                     // Save submission in offline.
 | ||||
|                     await AddonModWorkshopOffline.saveSubmission( | ||||
|                         this.workshopId, | ||||
|                         this.courseId, | ||||
|                         inputData.title, | ||||
|                         inputData.content, | ||||
|                         attachmentsId as CoreFileUploaderStoreFilesResult, | ||||
|                         submissionId, | ||||
|                         AddonModWorkshopAction.UPDATE, | ||||
|                     ); | ||||
|                     newSubmissionId = false; | ||||
|                 } else { | ||||
|                     // Try to send it to server.
 | ||||
|                     // Don't allow offline if there are attachments since they were uploaded fine.
 | ||||
|                     newSubmissionId = await AddonModWorkshop.updateSubmission( | ||||
|                         this.workshopId, | ||||
|                         submissionId!, | ||||
|                         this.courseId, | ||||
|                         inputData.title, | ||||
|                         inputData.content, | ||||
|                         attachmentsId, | ||||
|                         undefined, | ||||
|                         allowOffline, | ||||
|                     ); | ||||
|                 } | ||||
|             } else { | ||||
|                 if (saveOffline) { | ||||
|                     // Save submission in offline.
 | ||||
|                     await AddonModWorkshopOffline.saveSubmission( | ||||
|                         this.workshopId, | ||||
|                         this.courseId, | ||||
|                         inputData.title, | ||||
|                         inputData.content, | ||||
|                         attachmentsId as CoreFileUploaderStoreFilesResult, | ||||
|                         undefined, | ||||
|                         AddonModWorkshopAction.ADD, | ||||
|                     ); | ||||
|                     newSubmissionId = false; | ||||
|                 } else { | ||||
|                     // Try to send it to server.
 | ||||
|                     // Don't allow offline if there are attachments since they were uploaded fine.
 | ||||
|                     newSubmissionId = await AddonModWorkshop.addSubmission( | ||||
|                         this.workshopId, | ||||
|                         this.courseId, | ||||
|                         inputData.title, | ||||
|                         inputData.content, | ||||
|                         attachmentsId, | ||||
|                         undefined, | ||||
|                         allowOffline, | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             CoreForms.triggerFormSubmittedEvent(this.formElement, !!newSubmissionId, this.siteId); | ||||
| 
 | ||||
|             const data: AddonModWorkshopSubmissionChangedEventData = { | ||||
|                 workshopId: this.workshopId, | ||||
|             }; | ||||
| 
 | ||||
|             if (newSubmissionId) { | ||||
|                 // Data sent to server, delete stored files (if any).
 | ||||
|                 AddonModWorkshopOffline.deleteSubmissionAction( | ||||
|                     this.workshopId, | ||||
|                     this.editing ? AddonModWorkshopAction.UPDATE : AddonModWorkshopAction.ADD, | ||||
|                 ); | ||||
| 
 | ||||
|                 AddonModWorkshopHelper.deleteSubmissionStoredFiles(this.workshopId, this.siteId); | ||||
|                 data.submissionId = newSubmissionId; | ||||
|             } | ||||
| 
 | ||||
|             CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'workshop' }); | ||||
| 
 | ||||
|             const promise = newSubmissionId ? AddonModWorkshop.invalidateSubmissionData(this.workshopId, newSubmissionId) : | ||||
|                 Promise.resolve(); | ||||
| 
 | ||||
|             await promise.finally(() => { | ||||
|                 CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId); | ||||
| 
 | ||||
|                 // Delete the local files from the tmp folder.
 | ||||
|                 CoreFileUploader.clearTmpFiles(inputData.attachmentfiles); | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Cannot save submission'); | ||||
|         } finally { | ||||
|             modal.dismiss(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     protected getFilesComponentId(): string { | ||||
|         const id = this.submissionId > 0 | ||||
|             ? this.submissionId | ||||
|             : 'newsub'; | ||||
| 
 | ||||
|         return this.workshopId + '_' + id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
|         CoreSync.unblockOperation(this.component, this.workshopId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type AddonModWorkshopEditSubmissionInputData = { | ||||
|     title: string; | ||||
|     content: string; | ||||
|     attachmentfiles: CoreFileEntry[]; | ||||
| }; | ||||
							
								
								
									
										154
									
								
								src/addons/mod/workshop/pages/submission/submission.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/addons/mod/workshop/pages/submission/submission.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,154 @@ | ||||
| <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 *ngIf="title" [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end" [hidden]="!loaded"> | ||||
|             <ion-button *ngIf="assessmentId && access.assessingallowed" fill="clear" (click)="saveAssessment()" | ||||
|                 [attr.aria-label]="'core.save' | translate"> | ||||
|                 {{ 'core.save' | translate }} | ||||
|             </ion-button> | ||||
|             <ion-button *ngIf="canAddFeedback" fill="clear" (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate"> | ||||
|                 {{ 'core.save' | translate }} | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)" | ||||
|         *ngIf="!((assessmentId && access.assessingallowed) || canAddFeedback)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <ion-list *ngIf="submission"> | ||||
|             <addon-mod-workshop-submission [submission]="submission" [courseId]="courseId" [module]="module" [workshop]="workshop" | ||||
|                 [access]="access"> | ||||
|             </addon-mod-workshop-submission> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="canEdit || canDelete"> | ||||
|                 <ion-label> | ||||
|                     <ion-button expand="block" *ngIf="canEdit" (click)="editSubmission()"> | ||||
|                         <ion-icon name="fas-edit" slot="start"></ion-icon> | ||||
|                         {{ 'addon.mod_workshop.editsubmission' | translate }} | ||||
|                     </ion-button> | ||||
|                     <ion-button expand="block" *ngIf="!submission.deleted && canDelete" color="danger" (click)="deleteSubmission()"> | ||||
|                         <ion-icon name="fas-trash" slot="start"></ion-icon> | ||||
|                         {{ 'addon.mod_workshop.deletesubmission' | translate }} | ||||
|                     </ion-button> | ||||
|                     <ion-button expand="block" fill="outline" *ngIf="submission.deleted && canDelete" color="danger" | ||||
|                         (click)="undoDeleteSubmission()"> | ||||
|                         <ion-icon name="fas-undo-alt" slot="start"></ion-icon> | ||||
|                         {{ 'core.restore' | translate }} | ||||
|                     </ion-button> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <ion-list *ngIf="!canAddFeedback && evaluate?.text"> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <core-user-avatar *ngIf="evaluateByProfile" [user]="evaluateByProfile" slot="start" [courseId]="courseId" | ||||
|                     [userId]="evaluateByProfile.id"></core-user-avatar> | ||||
|                 <ion-label> | ||||
|                     <h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname"> | ||||
|                         {{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }} | ||||
|                     </h2> | ||||
|                     <core-format-text [text]="evaluate?.text" contextLevel="module" [contextInstanceId]="module.id" | ||||
|                         [courseId]="courseId"> | ||||
|                     </core-format-text> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <ion-list *ngIf="ownAssessment && !assessment"> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_workshop.yourassessment' | translate }}</h2> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <addon-mod-workshop-assessment [submission]="submission" [assessment]="ownAssessment" [courseId]="courseId" | ||||
|                 [access]="access" [module]="module" [workshop]="workshop"> | ||||
|             </addon-mod-workshop-assessment> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <ion-list *ngIf="submissionInfo && submissionInfo.reviewedby && submissionInfo.reviewedby.length && !assessment"> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_workshop.receivedgrades' | translate }}</h2> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ng-container *ngFor="let reviewer of submissionInfo.reviewedby"> | ||||
|                 <addon-mod-workshop-assessment *ngIf="!reviewer.ownAssessment" [submission]="submission" [assessment]="reviewer" | ||||
|                     [courseId]="courseId" [access]="access" [module]="module" [workshop]="workshop"> | ||||
|                 </addon-mod-workshop-assessment> | ||||
|             </ng-container> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <ion-list *ngIf="submissionInfo && submissionInfo.reviewerof && submissionInfo.reviewerof.length && !assessment"> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_workshop.givengrades' | translate }}</h2> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <addon-mod-workshop-assessment *ngFor="let reviewer of submissionInfo.reviewerof" [assessment]="reviewer" | ||||
|                 [courseId]="courseId" [module]="module" [workshop]="workshop" [access]="access"> | ||||
|             </addon-mod-workshop-assessment> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <form ion-list [formGroup]="feedbackForm" *ngIf="canAddFeedback && submission" #feedbackFormEl> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h2> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="access.canpublishsubmissions"> | ||||
|                 <ion-label>{{ 'addon.mod_workshop.publishsubmission' | translate }}</ion-label> | ||||
|                 <ion-toggle formControlName="published"></ion-toggle> | ||||
|                 <p class="item-help">{{ 'addon.mod_workshop.publishsubmission_help' | translate }}</p> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_workshop.gradecalculated' | translate }}</h2> | ||||
|                     <p>{{ submission.grade }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label position="stacked">{{ 'addon.mod_workshop.gradeover' | translate }}</ion-label> | ||||
|                 <ion-select formControlName="grade" interface="action-sheet"> | ||||
|                     <ion-select-option *ngFor="let grade of evaluationGrades" [value]="grade.value"> | ||||
|                         {{grade.label}} | ||||
|                     </ion-select-option> | ||||
|                 </ion-select> | ||||
|             </ion-item> | ||||
|             <ion-item> | ||||
|                 <ion-label position="stacked">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label> | ||||
|                 <core-rich-text-editor [control]="feedbackForm.controls['text']" name="text" | ||||
|                     [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id" elementId="feedbackauthor_editor" | ||||
|                     [draftExtraParams]="{id: submissionId}"> | ||||
|                 </core-rich-text-editor> | ||||
|             </ion-item> | ||||
|         </form> | ||||
| 
 | ||||
|         <addon-mod-workshop-assessment-strategy *ngIf="assessmentId" [workshop]="workshop" [access]="access" | ||||
|             [assessmentId]="assessmentId" [userId]="assessmentUserId" [strategy]="strategy" [edit]="access.assessingallowed"> | ||||
|         </addon-mod-workshop-assessment-strategy> | ||||
| 
 | ||||
|         <ion-list *ngIf="assessmentId && !access.assessingallowed && assessment?.feedbackreviewer"> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <core-user-avatar *ngIf="evaluateGradingByProfile" [user]="evaluateGradingByProfile" slot="start" | ||||
|                     [courseId]="courseId" [userId]="evaluateGradingByProfile.id"></core-user-avatar> | ||||
|                 <ion-label> | ||||
|                     <h2 *ngIf="evaluateGradingByProfile && evaluateGradingByProfile.fullname"> | ||||
|                         {{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateGradingByProfile.fullname} }} | ||||
|                     </h2> | ||||
|                     <core-format-text [text]="assessment!.feedbackreviewer" contextLevel="module" [contextInstanceId]="module.id" | ||||
|                         [courseId]="courseId"> | ||||
|                     </core-format-text> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										610
									
								
								src/addons/mod/workshop/pages/submission/submission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										610
									
								
								src/addons/mod/workshop/pages/submission/submission.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,610 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, OnDestroy, Optional, ViewChild, ElementRef } from '@angular/core'; | ||||
| import { FormGroup, FormBuilder } from '@angular/forms'; | ||||
| import { Params } from '@angular/router'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { CoreCourseModule } from '@features/course/services/course-helper'; | ||||
| import { CoreGradesHelper, CoreGradesMenuItem } from '@features/grades/services/grades-helper'; | ||||
| import { CoreUser, CoreUserProfile } from '@features/user/services/user'; | ||||
| import { CanLeave } from '@guards/can-leave'; | ||||
| import { IonContent, IonRefresher } from '@ionic/angular'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreForms } from '@singletons/form'; | ||||
| import { AddonModWorkshopAssessmentStrategyComponent } from '../../components/assessment-strategy/assessment-strategy'; | ||||
| import { | ||||
|     AddonModWorkshopProvider, | ||||
|     AddonModWorkshop, | ||||
|     AddonModWorkshopPhase, | ||||
|     AddonModWorkshopSubmissionChangedEventData, | ||||
|     AddonModWorkshopAction, | ||||
|     AddonModWorkshopData, | ||||
|     AddonModWorkshopGetWorkshopAccessInformationWSResponse, | ||||
|     AddonModWorkshopAssessmentSavedChangedEventData, | ||||
| } from '../../services/workshop'; | ||||
| import { | ||||
|     AddonModWorkshopHelper, | ||||
|     AddonModWorkshopSubmissionAssessmentWithFormData, | ||||
|     AddonModWorkshopSubmissionDataWithOfflineData, | ||||
| } from '../../services/workshop-helper'; | ||||
| import { AddonModWorkshopOffline } from '../../services/workshop-offline'; | ||||
| import { AddonModWorkshopSyncProvider, AddonModWorkshopAutoSyncData } from '../../services/workshop-sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays a workshop submission. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-workshop-submission-page', | ||||
|     templateUrl: 'submission.html', | ||||
| }) | ||||
| export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLeave { | ||||
| 
 | ||||
|     @ViewChild(AddonModWorkshopAssessmentStrategyComponent) assessmentStrategy?: AddonModWorkshopAssessmentStrategyComponent; | ||||
| 
 | ||||
|     @ViewChild('feedbackFormEl') formElement?: ElementRef; | ||||
| 
 | ||||
|     module!: CoreCourseModule; | ||||
|     workshop!: AddonModWorkshopData; | ||||
|     access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse; | ||||
|     assessment?: AddonModWorkshopSubmissionAssessmentWithFormData; | ||||
|     submissionInfo!: AddonModWorkshopSubmissionDataWithOfflineData; | ||||
|     profile?: CoreUserProfile; | ||||
|     courseId!: number; | ||||
| 
 | ||||
|     submission?: AddonModWorkshopSubmissionDataWithOfflineData; | ||||
|     title?: string; | ||||
|     loaded = false; | ||||
|     ownAssessment?: AddonModWorkshopSubmissionAssessmentWithFormData; | ||||
|     strategy?: string; | ||||
|     assessmentId?: number; | ||||
|     assessmentUserId?: number; | ||||
|     evaluate?: AddonWorkshopSubmissionEvaluateData; | ||||
|     canAddFeedback = false; | ||||
|     canEdit = false; | ||||
|     canDelete = false; | ||||
|     evaluationGrades: CoreGradesMenuItem[] = []; | ||||
|     evaluateGradingByProfile?: CoreUserProfile; | ||||
|     evaluateByProfile?: CoreUserProfile; | ||||
|     feedbackForm: FormGroup; // The form group.
 | ||||
|     submissionId!: number; | ||||
| 
 | ||||
|     protected workshopId!: number; | ||||
|     protected currentUserId: number; | ||||
|     protected userId?: number; | ||||
|     protected siteId: string; | ||||
|     protected originalEvaluation: Omit<AddonWorkshopSubmissionEvaluateData, 'grade'> & { grade: number | string} = { | ||||
|         published: false, | ||||
|         text: '', | ||||
|         grade: '', | ||||
|     }; | ||||
| 
 | ||||
|     protected hasOffline = false; | ||||
|     protected component = AddonModWorkshopProvider.COMPONENT; | ||||
|     protected forceLeave = false; | ||||
|     protected obsAssessmentSaved: CoreEventObserver; | ||||
|     protected syncObserver: CoreEventObserver; | ||||
|     protected isDestroyed = false; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected fb: FormBuilder, | ||||
|         @Optional() protected content: IonContent, | ||||
|     ) { | ||||
|         this.currentUserId = CoreSites.getCurrentSiteUserId(); | ||||
|         this.siteId = CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         this.feedbackForm = new FormGroup({}); | ||||
|         this.feedbackForm.addControl('published', this.fb.control('')); | ||||
|         this.feedbackForm.addControl('grade', this.fb.control('')); | ||||
|         this.feedbackForm.addControl('text', this.fb.control('')); | ||||
| 
 | ||||
|         this.obsAssessmentSaved = CoreEvents.on(AddonModWorkshopProvider.ASSESSMENT_SAVED, (data) => { | ||||
|             this.eventReceived(data); | ||||
|         }, this.siteId); | ||||
| 
 | ||||
|         // Refresh workshop on sync.
 | ||||
|         this.syncObserver = CoreEvents.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => { | ||||
|             // Update just when all database is synced.
 | ||||
|             this.eventReceived(data); | ||||
|         }, this.siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
| 
 | ||||
|         this.submissionId = CoreNavigator.getRouteNumberParam('submissionId')!; | ||||
|         this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!; | ||||
|         this.workshop = CoreNavigator.getRouteParam<AddonModWorkshopData>('workshop')!; | ||||
|         this.access = CoreNavigator.getRouteParam<AddonModWorkshopGetWorkshopAccessInformationWSResponse>('access')!; | ||||
|         this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; | ||||
|         this.profile = CoreNavigator.getRouteParam<CoreUserProfile>('profile'); | ||||
|         this.submissionInfo = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionDataWithOfflineData>('submission')!; | ||||
|         this.assessment = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionAssessmentWithFormData>('assessment'); | ||||
| 
 | ||||
|         this.title = this.module.name; | ||||
|         this.workshopId = this.module.instance || this.workshop.id; | ||||
| 
 | ||||
|         this.userId = this.submissionInfo?.authorid; | ||||
|         this.strategy = (this.assessment && this.assessment.strategy) || (this.workshop && this.workshop.strategy); | ||||
|         this.assessmentId = this.assessment?.id; | ||||
|         this.assessmentUserId = this.assessment?.reviewerid; | ||||
| 
 | ||||
|         await this.fetchSubmissionData(); | ||||
| 
 | ||||
|         try { | ||||
|             await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId, this.workshop.name); | ||||
|             CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); | ||||
|         } catch { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we can leave the page or not. | ||||
|      * | ||||
|      * @return Resolved if we can leave it, rejected if not. | ||||
|      */ | ||||
|     async canLeave(): Promise<boolean> { | ||||
|         const assessmentHasChanged = this.assessmentStrategy?.hasDataChanged(); | ||||
|         if (this.forceLeave || (!this.hasEvaluationChanged() && !assessmentHasChanged)) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // Show confirmation if some data has been modified.
 | ||||
|         await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); | ||||
| 
 | ||||
|         CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Goto edit submission page. | ||||
|      */ | ||||
|     editSubmission(): void { | ||||
|         const params: Params = { | ||||
|             module: module, | ||||
|             access: this.access, | ||||
|         }; | ||||
| 
 | ||||
|         CoreNavigator.navigate(String(this.submissionId) + '/edit', params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function called when we receive an event of submission changes. | ||||
|      * | ||||
|      * @param data Event data received. | ||||
|      */ | ||||
|     protected eventReceived(data: AddonModWorkshopAutoSyncData | | ||||
|     AddonModWorkshopAssessmentSavedChangedEventData): void { | ||||
|         if (this.workshopId === data.workshopId) { | ||||
|             this.content?.scrollToTop(); | ||||
| 
 | ||||
|             this.loaded = false; | ||||
|             this.refreshAllData(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the submission data. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async fetchSubmissionData(): Promise<void> { | ||||
|         try { | ||||
|             this.submission = await AddonModWorkshopHelper.getSubmissionById(this.workshopId, this.submissionId, { | ||||
|                 cmId: this.module.id, | ||||
|             }); | ||||
| 
 | ||||
|             const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|             this.submission.grade = this.submissionInfo?.grade; | ||||
|             this.submission.gradinggrade = this.submissionInfo?.gradinggrade; | ||||
|             this.submission.gradeover = this.submissionInfo?.gradeover; | ||||
|             this.userId = this.submission.authorid || this.userId; | ||||
|             this.canEdit = this.currentUserId == this.userId && this.access.cansubmit && this.access.modifyingsubmissionallowed; | ||||
|             this.canDelete = this.access.candeletesubmissions; | ||||
| 
 | ||||
|             this.canAddFeedback = !this.assessmentId && this.workshop.phase > AddonModWorkshopPhase.PHASE_ASSESSMENT && | ||||
|                 this.workshop.phase < AddonModWorkshopPhase.PHASE_CLOSED && this.access.canoverridegrades; | ||||
|             this.ownAssessment = undefined; | ||||
| 
 | ||||
|             if (this.access.canviewallassessments) { | ||||
|                 // Get new data, different that came from stateParams.
 | ||||
|                 promises.push(AddonModWorkshop.getSubmissionAssessments(this.workshopId, this.submissionId, { | ||||
|                     cmId: this.module.id, | ||||
|                 }).then((subAssessments) => { | ||||
|                     // Only allow the student to delete their own submission if it's still editable and hasn't been assessed.
 | ||||
|                     if (this.canDelete) { | ||||
|                         this.canDelete = !subAssessments.length; | ||||
|                     } | ||||
| 
 | ||||
|                     this.submissionInfo.reviewedby = subAssessments; | ||||
| 
 | ||||
|                     this.submissionInfo.reviewedby.forEach((assessment) => { | ||||
|                         assessment = AddonModWorkshopHelper.realGradeValue(this.workshop, assessment); | ||||
| 
 | ||||
|                         if (this.currentUserId == assessment.reviewerid) { | ||||
|                             this.ownAssessment = assessment; | ||||
|                             assessment.ownAssessment = true; | ||||
|                         } | ||||
|                     }); | ||||
| 
 | ||||
|                     return; | ||||
|                 })); | ||||
|             } else if (this.currentUserId == this.userId && this.assessmentId) { | ||||
|                 // Get new data, different that came from stateParams.
 | ||||
|                 promises.push(AddonModWorkshop.getAssessment(this.workshopId, this.assessmentId, { | ||||
|                     cmId: this.module.id, | ||||
|                 }).then((assessment) => { | ||||
|                     // Only allow the student to delete their own submission if it's still editable and hasn't been assessed.
 | ||||
|                     if (this.canDelete) { | ||||
|                         this.canDelete = !assessment; | ||||
|                     } | ||||
| 
 | ||||
|                     this.submissionInfo.reviewedby = [this.parseAssessment(assessment)]; | ||||
| 
 | ||||
|                     return; | ||||
|                 })); | ||||
|             } else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.userId == this.currentUserId) { | ||||
|                 const assessments = await AddonModWorkshop.getSubmissionAssessments(this.workshopId, this.submissionId, { | ||||
|                     cmId: this.module.id, | ||||
|                 }); | ||||
| 
 | ||||
|                 this.submissionInfo.reviewedby = assessments.map((assessment) => this.parseAssessment(assessment)); | ||||
|             } | ||||
| 
 | ||||
|             if (this.canAddFeedback || this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED) { | ||||
|                 this.evaluate = { | ||||
|                     published: this.submission.published, | ||||
|                     text: this.submission.feedbackauthor || '', | ||||
|                 }; | ||||
|             } | ||||
| 
 | ||||
|             if (this.canAddFeedback) { | ||||
| 
 | ||||
|                 if (!this.isDestroyed) { | ||||
|                     // Block the workshop.
 | ||||
|                     CoreSync.blockOperation(this.component, this.workshopId); | ||||
|                 } | ||||
| 
 | ||||
|                 const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden'); | ||||
| 
 | ||||
|                 promises.push(CoreGradesHelper.makeGradesMenu(this.workshop.grade || 0, undefined, defaultGrade, -1) | ||||
|                     .then(async (grades) => { | ||||
|                         this.evaluationGrades = grades; | ||||
| 
 | ||||
|                         this.evaluate!.grade = { | ||||
|                             label: CoreGradesHelper.getGradeLabelFromValue(grades, this.submissionInfo.gradeover) || | ||||
|                             defaultGrade, | ||||
|                             value: this.submissionInfo.gradeover || -1, | ||||
|                         }; | ||||
| 
 | ||||
|                         try { | ||||
|                             const offlineSubmission = | ||||
|                                 await AddonModWorkshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId); | ||||
| 
 | ||||
|                             this.hasOffline = true; | ||||
|                             this.evaluate!.published = offlineSubmission.published; | ||||
|                             this.evaluate!.text = offlineSubmission.feedbacktext; | ||||
|                             this.evaluate!.grade = { | ||||
|                                 label: CoreGradesHelper.getGradeLabelFromValue( | ||||
|                                     grades, | ||||
|                                     parseInt(offlineSubmission.gradeover, 10), | ||||
|                                 ) || defaultGrade, | ||||
|                                 value: offlineSubmission.gradeover || -1, | ||||
|                             }; | ||||
|                         } catch { | ||||
|                             // Ignore errors.
 | ||||
|                             this.hasOffline = false; | ||||
|                         } finally { | ||||
|                             this.originalEvaluation.published = this.evaluate!.published; | ||||
|                             this.originalEvaluation.text = this.evaluate!.text; | ||||
|                             this.originalEvaluation.grade = this.evaluate!.grade.value; | ||||
| 
 | ||||
|                             this.feedbackForm.controls['published'].setValue(this.evaluate!.published); | ||||
|                             this.feedbackForm.controls['grade'].setValue(this.evaluate!.grade.value); | ||||
|                             this.feedbackForm.controls['text'].setValue(this.evaluate!.text); | ||||
|                         } | ||||
| 
 | ||||
|                         return; | ||||
|                     })); | ||||
|             } else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.submission.gradeoverby && | ||||
|                     this.evaluate && this.evaluate.text) { | ||||
|                 promises.push(CoreUser.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => { | ||||
|                     this.evaluateByProfile = profile; | ||||
| 
 | ||||
|                     return; | ||||
|                 })); | ||||
|             } | ||||
| 
 | ||||
|             if (this.assessment && !this.access.assessingallowed && this.assessment.feedbackreviewer && | ||||
|                     this.assessment.gradinggradeoverby) { | ||||
|                 promises.push(CoreUser.getProfile(this.assessment.gradinggradeoverby, this.courseId, true) | ||||
|                     .then((profile) => { | ||||
|                         this.evaluateGradingByProfile = profile; | ||||
| 
 | ||||
|                         return; | ||||
|                     })); | ||||
|             } | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
| 
 | ||||
|             const submissionsActions = await AddonModWorkshopOffline.getSubmissions(this.workshopId); | ||||
| 
 | ||||
|             this.submission = await AddonModWorkshopHelper.applyOfflineData(this.submission, submissionsActions); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse assessment to be shown. | ||||
|      * | ||||
|      * @param  assessment Original assessment. | ||||
|      * @return Parsed assessment. | ||||
|      */ | ||||
|     protected parseAssessment( | ||||
|         assessment: AddonModWorkshopSubmissionAssessmentWithFormData, | ||||
|     ): AddonModWorkshopSubmissionAssessmentWithFormData { | ||||
|         assessment = AddonModWorkshopHelper.realGradeValue(this.workshop, assessment); | ||||
| 
 | ||||
|         if (this.currentUserId == assessment.reviewerid) { | ||||
|             this.ownAssessment = assessment; | ||||
|             assessment.ownAssessment = true; | ||||
|         } | ||||
| 
 | ||||
|         return assessment; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Force leaving the page, without checking for changes. | ||||
|      */ | ||||
|     protected forceLeavePage(): void { | ||||
|         this.forceLeave = true; | ||||
|         CoreNavigator.back(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if data has changed. | ||||
|      * | ||||
|      * @return True if changed, false otherwise. | ||||
|      */ | ||||
|     protected hasEvaluationChanged(): boolean { | ||||
|         if (!this.loaded || !this.access.canoverridegrades) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const inputData = this.feedbackForm.value; | ||||
| 
 | ||||
|         if (this.originalEvaluation.published != inputData.published) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (this.originalEvaluation.text != inputData.text) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (this.originalEvaluation.grade != inputData.grade) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to refresh all the data. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async refreshAllData(): Promise<void> { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId)); | ||||
|         promises.push(AddonModWorkshop.invalidateSubmissionsData(this.workshopId)); | ||||
|         promises.push(AddonModWorkshop.invalidateSubmissionAssesmentsData(this.workshopId, this.submissionId)); | ||||
| 
 | ||||
|         if (this.assessmentId) { | ||||
|             promises.push(AddonModWorkshop.invalidateAssessmentFormData(this.workshopId, this.assessmentId)); | ||||
|             promises.push(AddonModWorkshop.invalidateAssessmentData(this.workshopId, this.assessmentId)); | ||||
|         } | ||||
| 
 | ||||
|         if (this.assessmentUserId) { | ||||
|             promises.push(AddonModWorkshop.invalidateReviewerAssesmentsData(this.workshopId, this.assessmentId)); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await Promise.all(promises); | ||||
|         } finally { | ||||
|             CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, null, this.siteId); | ||||
| 
 | ||||
|             await this.fetchSubmissionData(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Pull to refresh. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      */ | ||||
|     refreshSubmission(refresher: IonRefresher): void { | ||||
|         if (this.loaded) { | ||||
|             this.refreshAllData().finally(() => { | ||||
|                 refresher?.complete(); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the assessment. | ||||
|      */ | ||||
|     async saveAssessment(): Promise<void> { | ||||
|         if (this.assessmentStrategy?.hasDataChanged()) { | ||||
|             try { | ||||
|                 await this.assessmentStrategy.saveAssessment(); | ||||
|                 this.forceLeavePage(); | ||||
|             } catch { | ||||
|                 // Error, stay on the page.
 | ||||
|             } | ||||
|         } else { | ||||
|             // Nothing to save, just go back.
 | ||||
|             this.forceLeavePage(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the submission evaluation. | ||||
|      */ | ||||
|     async saveEvaluation(): Promise<void> { | ||||
|         // Check if data has changed.
 | ||||
|         if (this.hasEvaluationChanged()) { | ||||
|             await this.sendEvaluation(); | ||||
|             this.forceLeavePage(); | ||||
|         } else { | ||||
|             // Nothing to save, just go back.
 | ||||
|             this.forceLeavePage(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sends the evaluation to be saved on the server. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async sendEvaluation(): Promise<void> { | ||||
|         const modal = await CoreDomUtils.showModalLoading('core.sending', true); | ||||
| 
 | ||||
|         const inputData: { | ||||
|             grade: number | string; | ||||
|             text: string; | ||||
|             published: boolean; | ||||
|         } = this.feedbackForm.value; | ||||
| 
 | ||||
|         inputData.grade = inputData.grade >= 0 ? inputData.grade : ''; | ||||
|         // Add some HTML to the message if needed.
 | ||||
|         inputData.text = CoreTextUtils.formatHtmlLines(inputData.text); | ||||
| 
 | ||||
|         // Try to send it to server.
 | ||||
|         try { | ||||
|             const result = await AddonModWorkshop.evaluateSubmission( | ||||
|                 this.workshopId, | ||||
|                 this.submissionId, | ||||
|                 this.courseId, | ||||
|                 inputData.text, | ||||
|                 inputData.published, | ||||
|                 String(inputData.grade), | ||||
|             ); | ||||
|             CoreForms.triggerFormSubmittedEvent(this.formElement, !!result, this.siteId); | ||||
| 
 | ||||
|             await AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId).finally(() => { | ||||
|                 const data: AddonModWorkshopSubmissionChangedEventData = { | ||||
|                     workshopId: this.workshopId, | ||||
|                     submissionId: this.submissionId, | ||||
|                 }; | ||||
| 
 | ||||
|                 CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId); | ||||
|             }); | ||||
|         } catch (message) { | ||||
|             CoreDomUtils.showErrorModalDefault(message, 'Cannot save submission evaluation'); | ||||
|         } finally { | ||||
|             modal.dismiss(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the submission delete action. | ||||
|      */ | ||||
|     async deleteSubmission(): Promise<void> { | ||||
|         try { | ||||
|             await CoreDomUtils.showDeleteConfirm('addon.mod_workshop.submissiondeleteconfirm'); | ||||
|         } catch { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const modal = await CoreDomUtils.showModalLoading('core.deleting', true); | ||||
| 
 | ||||
|         let success = false; | ||||
|         try { | ||||
|             await AddonModWorkshop.deleteSubmission(this.workshopId, this.submissionId, this.courseId); | ||||
|             success = true; | ||||
| 
 | ||||
|             await AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Cannot delete submission'); | ||||
|         } finally { | ||||
|             modal.dismiss(); | ||||
|             if (success) { | ||||
|                 const data: AddonModWorkshopSubmissionChangedEventData = { | ||||
|                     workshopId: this.workshopId, | ||||
|                     submissionId: this.submissionId, | ||||
|                 }; | ||||
| 
 | ||||
|                 CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId); | ||||
| 
 | ||||
|                 this.forceLeavePage(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Undo the submission delete action. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     async undoDeleteSubmission(): Promise<void> { | ||||
|         await AddonModWorkshopOffline.deleteSubmissionAction( | ||||
|             this.workshopId, | ||||
|             AddonModWorkshopAction.DELETE, | ||||
|         ).finally(async () => { | ||||
| 
 | ||||
|             const data: AddonModWorkshopSubmissionChangedEventData = { | ||||
|                 workshopId: this.workshopId, | ||||
|                 submissionId: this.submissionId, | ||||
|             }; | ||||
| 
 | ||||
|             CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId); | ||||
| 
 | ||||
|             await this.refreshAllData(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
| 
 | ||||
|         this.syncObserver?.off(); | ||||
|         this.obsAssessmentSaved?.off(); | ||||
|         // Restore original back functions.
 | ||||
|         CoreSync.unblockOperation(this.component, this.workshopId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type AddonWorkshopSubmissionEvaluateData = { | ||||
|     published: boolean; | ||||
|     text: string; | ||||
|     grade?: CoreGradesMenuItem; | ||||
| }; | ||||
							
								
								
									
										159
									
								
								src/addons/mod/workshop/services/assessment-strategy-delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/addons/mod/workshop/services/assessment-strategy-delegate.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,159 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { CoreDelegateHandler, CoreDelegate } from '@classes/delegate'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreFormFields } from '@singletons/form'; | ||||
| import { AddonModWorkshopGetAssessmentFormDefinitionData, AddonModWorkshopGetAssessmentFormFieldsParsedData } from './workshop'; | ||||
| 
 | ||||
| /** | ||||
|  * Interface that all assessment strategy handlers must implement. | ||||
|  */ | ||||
| export interface AddonWorkshopAssessmentStrategyHandler extends CoreDelegateHandler { | ||||
|     /** | ||||
|      * The name of the assessment strategy. E.g. 'accumulative'. | ||||
|      */ | ||||
|     strategyName: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to render the plugin. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @param injector Injector. | ||||
|      * @return The component (or promise resolved with component) to use, undefined if not found. | ||||
|      */ | ||||
|     getComponent?(): Type<unknown>; | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare original values to be shown and compared. | ||||
|      * | ||||
|      * @param form Original data of the form. | ||||
|      * @param workshopId WorkShop Id | ||||
|      * @return Promise resolved with original values sorted. | ||||
|      */ | ||||
|     getOriginalValues?( | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|         workshopId: number, | ||||
|     ): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]>; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin. | ||||
|      * | ||||
|      * @param originalValues Original values of the form. | ||||
|      * @param currentValues Current values of the form. | ||||
|      * @return True if data has changed, false otherwise. | ||||
|      */ | ||||
|     hasDataChanged?( | ||||
|         originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|     ): boolean; | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare assessment data to be sent to the server depending on the strategy selected. | ||||
|      * | ||||
|      * @param currentValues Current values of the form. | ||||
|      * @param form Assessment form data. | ||||
|      * @return Promise resolved with the data to be sent. Or rejected with the input errors object. | ||||
|      */ | ||||
|     prepareAssessmentData( | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<CoreFormFields<unknown>>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Delegate to register workshop assessment strategy handlers. | ||||
|  * You can use this service to register your own assessment strategy handlers to be used in a workshop. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonWorkshopAssessmentStrategyDelegateService extends CoreDelegate<AddonWorkshopAssessmentStrategyHandler> { | ||||
| 
 | ||||
|     protected handlerNameProperty = 'strategyName'; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super('AddonWorkshopAssessmentStrategyDelegate', true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if an assessment strategy plugin is supported. | ||||
|      * | ||||
|      * @param workshopStrategy Assessment strategy name. | ||||
|      * @return True if supported, false otherwise. | ||||
|      */ | ||||
|     isPluginSupported(workshopStrategy: string): boolean { | ||||
|         return this.hasHandler(workshopStrategy, true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the directive to use for a certain assessment strategy plugin. | ||||
|      * | ||||
|      * @param injector Injector. | ||||
|      * @param workshopStrategy Assessment strategy name. | ||||
|      * @return The component, undefined if not found. | ||||
|      */ | ||||
|     getComponentForPlugin(workshopStrategy: string): Type<unknown> | undefined { | ||||
|         return this.executeFunctionOnEnabled(workshopStrategy, 'getComponent'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare original values to be shown and compared depending on the strategy selected. | ||||
|      * | ||||
|      * @param workshopStrategy Workshop strategy. | ||||
|      * @param form Original data of the form. | ||||
|      * @param workshopId Workshop ID. | ||||
|      * @return Resolved with original values sorted. | ||||
|      */ | ||||
|     getOriginalValues( | ||||
|         workshopStrategy: string, | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|         workshopId: number, | ||||
|     ): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> { | ||||
|         return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'getOriginalValues', [form, workshopId]) || []); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin. | ||||
|      * | ||||
|      * @param workshopStrategy Workshop strategy. | ||||
|      * @param originalValues Original values of the form. | ||||
|      * @param currentValues Current values of the form. | ||||
|      * @return True if data has changed, false otherwise. | ||||
|      */ | ||||
|     hasDataChanged( | ||||
|         workshopStrategy: string, | ||||
|         originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|     ): boolean { | ||||
|         return this.executeFunctionOnEnabled(workshopStrategy, 'hasDataChanged', [originalValues, currentValues]) || false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare assessment data to be sent to the server depending on the strategy selected. | ||||
|      * | ||||
|      * @param workshopStrategy Workshop strategy to follow. | ||||
|      * @param currentValues Current values of the form. | ||||
|      * @param form Assessment form data. | ||||
|      * @return Promise resolved with the data to be sent. Or rejected with the input errors object. | ||||
|      */ | ||||
|     prepareAssessmentData( | ||||
|         workshopStrategy: string, | ||||
|         currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[], | ||||
|         form: AddonModWorkshopGetAssessmentFormDefinitionData, | ||||
|     ): Promise<CoreFormFields<unknown> | undefined> { | ||||
|         return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'prepareAssessmentData', [currentValues, form])); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| export const AddonWorkshopAssessmentStrategyDelegate = makeSingleton(AddonWorkshopAssessmentStrategyDelegateService); | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user