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> |     <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" |     <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> |         contextLevel="module" [contextInstanceId]="componentId" [elementId]="'field_'+field.id" ngDefaultControl> | ||||||
|     </core-rich-text-editor> |     </core-rich-text-editor> | ||||||
|     <core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors> |     <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