diff --git a/src/addons/mod/data/fields/textarea/component/addon-mod-data-field-textarea.html b/src/addons/mod/data/fields/textarea/component/addon-mod-data-field-textarea.html index 9083dbec8..73d83c17d 100644 --- a/src/addons/mod/data/fields/textarea/component/addon-mod-data-field-textarea.html +++ b/src/addons/mod/data/fields/textarea/component/addon-mod-data-field-textarea.html @@ -3,7 +3,7 @@ diff --git a/src/addons/mod/workshop/assessment/accumulative/accumulative.module.ts b/src/addons/mod/workshop/assessment/accumulative/accumulative.module.ts new file mode 100644 index 000000000..7eaf6ccc4 --- /dev/null +++ b/src/addons/mod/workshop/assessment/accumulative/accumulative.module.ts @@ -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 {} diff --git a/src/addons/mod/workshop/assessment/accumulative/component/accumulative.ts b/src/addons/mod/workshop/assessment/accumulative/component/accumulative.ts new file mode 100644 index 000000000..24f912884 --- /dev/null +++ b/src/addons/mod/workshop/assessment/accumulative/component/accumulative.ts @@ -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 { } diff --git a/src/addons/mod/workshop/assessment/accumulative/component/addon-mod-workshop-assessment-strategy-accumulative.html b/src/addons/mod/workshop/assessment/accumulative/component/addon-mod-workshop-assessment-strategy-accumulative.html new file mode 100644 index 000000000..ccb21f9c3 --- /dev/null +++ b/src/addons/mod/workshop/assessment/accumulative/component/addon-mod-workshop-assessment-strategy-accumulative.html @@ -0,0 +1,50 @@ + + + + +

{{ field.dimtitle }}

+ + +
+
+ + + + {{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }} + + + + {{grade.label}} + + + + + + +

{{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }}

+ +

{{grade.label}}

+
+
+
+ + + {{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} + + + + + +

+ {{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} +

+

+ + +

+
+
+
+
diff --git a/src/addons/mod/workshop/assessment/accumulative/lang.json b/src/addons/mod/workshop/assessment/accumulative/lang.json new file mode 100644 index 000000000..72c26cfa8 --- /dev/null +++ b/src/addons/mod/workshop/assessment/accumulative/lang.json @@ -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" +} diff --git a/src/addons/mod/workshop/assessment/accumulative/services/handler.ts b/src/addons/mod/workshop/assessment/accumulative/services/handler.ts new file mode 100644 index 000000000..e62a71e8e --- /dev/null +++ b/src/addons/mod/workshop/assessment/accumulative/services/handler.ts @@ -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 { + return true; + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return AddonModWorkshopAssessmentStrategyAccumulativeComponent; + } + + /** + * @inheritdoc + */ + async getOriginalValues( + form: AddonModWorkshopGetAssessmentFormDefinitionData, + ): Promise { + const defaultGrade = Translate.instant('core.choosedots'); + const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = []; + const promises: Promise[] = []; + + 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 { + 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); diff --git a/src/addons/mod/workshop/assessment/assessment.module.ts b/src/addons/mod/workshop/assessment/assessment.module.ts new file mode 100644 index 000000000..dedb5952d --- /dev/null +++ b/src/addons/mod/workshop/assessment/assessment.module.ts @@ -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 {} diff --git a/src/addons/mod/workshop/assessment/comments/comments.module.ts b/src/addons/mod/workshop/assessment/comments/comments.module.ts new file mode 100644 index 000000000..2ceaf9a5a --- /dev/null +++ b/src/addons/mod/workshop/assessment/comments/comments.module.ts @@ -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 {} diff --git a/src/addons/mod/workshop/assessment/comments/component/addon-mod-workshop-assessment-strategy-comments.html b/src/addons/mod/workshop/assessment/comments/component/addon-mod-workshop-assessment-strategy-comments.html new file mode 100644 index 000000000..74aaacb70 --- /dev/null +++ b/src/addons/mod/workshop/assessment/comments/component/addon-mod-workshop-assessment-strategy-comments.html @@ -0,0 +1,30 @@ + + + + +

{{ field.dimtitle }}

+ + +
+
+ + + + {{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} + + + + + + + + +

{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}

+

+

+
+
+
+
diff --git a/src/addons/mod/workshop/assessment/comments/component/comments.ts b/src/addons/mod/workshop/assessment/comments/component/comments.ts new file mode 100644 index 000000000..ebfc620b3 --- /dev/null +++ b/src/addons/mod/workshop/assessment/comments/component/comments.ts @@ -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 { } diff --git a/src/addons/mod/workshop/assessment/comments/lang.json b/src/addons/mod/workshop/assessment/comments/lang.json new file mode 100644 index 000000000..6db857323 --- /dev/null +++ b/src/addons/mod/workshop/assessment/comments/lang.json @@ -0,0 +1,4 @@ +{ + "dimensioncommentfor": "Comment for {{$a}}", + "dimensionnumber": "Aspect {{$a}}" +} diff --git a/src/addons/mod/workshop/assessment/comments/services/handler.ts b/src/addons/mod/workshop/assessment/comments/services/handler.ts new file mode 100644 index 000000000..8ab2448cd --- /dev/null +++ b/src/addons/mod/workshop/assessment/comments/services/handler.ts @@ -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 { + return true; + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return AddonModWorkshopAssessmentStrategyCommentsComponent; + } + + /** + * @inheritdoc + */ + async getOriginalValues( + form: AddonModWorkshopGetAssessmentFormDefinitionData, + ): Promise { + 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 { + 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); diff --git a/src/addons/mod/workshop/assessment/numerrors/component/addon-mod-workshop-assessment-strategy-numerrors.html b/src/addons/mod/workshop/assessment/numerrors/component/addon-mod-workshop-assessment-strategy-numerrors.html new file mode 100644 index 000000000..7a83404f8 --- /dev/null +++ b/src/addons/mod/workshop/assessment/numerrors/component/addon-mod-workshop-assessment-strategy-numerrors.html @@ -0,0 +1,53 @@ + + + + +

{{ field.dimtitle }}

+ + +
+
+ + + + + + {{ 'addon.mod_workshop.yourassessmentfor' | translate : {'$a': field.dimtitle } }} + + + + + + + + + + + + + + + + + + + + {{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} + + + + + + +

{{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}

+

+ + +

+
+
+
+
diff --git a/src/addons/mod/workshop/assessment/numerrors/component/numerrors.ts b/src/addons/mod/workshop/assessment/numerrors/component/numerrors.ts new file mode 100644 index 000000000..4f3d37d57 --- /dev/null +++ b/src/addons/mod/workshop/assessment/numerrors/component/numerrors.ts @@ -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 { } diff --git a/src/addons/mod/workshop/assessment/numerrors/lang.json b/src/addons/mod/workshop/assessment/numerrors/lang.json new file mode 100644 index 000000000..d12b03206 --- /dev/null +++ b/src/addons/mod/workshop/assessment/numerrors/lang.json @@ -0,0 +1,5 @@ +{ + "dimensioncommentfor": "Comment for {{$a}}", + "dimensiongradefor": "Grade for {{$a}}", + "dimensionnumber": "Assertion {{$a}}" +} diff --git a/src/addons/mod/workshop/assessment/numerrors/numerrors.module.ts b/src/addons/mod/workshop/assessment/numerrors/numerrors.module.ts new file mode 100644 index 000000000..dab6f1f38 --- /dev/null +++ b/src/addons/mod/workshop/assessment/numerrors/numerrors.module.ts @@ -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 {} diff --git a/src/addons/mod/workshop/assessment/numerrors/services/handler.ts b/src/addons/mod/workshop/assessment/numerrors/services/handler.ts new file mode 100644 index 000000000..195998169 --- /dev/null +++ b/src/addons/mod/workshop/assessment/numerrors/services/handler.ts @@ -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 { + return true; + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return AddonModWorkshopAssessmentStrategyNumErrorsComponent; + } + + /** + * @inheritdoc + */ + async getOriginalValues( + form: AddonModWorkshopGetAssessmentFormDefinitionData, + ): Promise { + 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 { + 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); diff --git a/src/addons/mod/workshop/assessment/rubric/component/addon-mod-workshop-assessment-strategy-rubric.html b/src/addons/mod/workshop/assessment/rubric/component/addon-mod-workshop-assessment-strategy-rubric.html new file mode 100644 index 000000000..1b4387116 --- /dev/null +++ b/src/addons/mod/workshop/assessment/rubric/component/addon-mod-workshop-assessment-strategy-rubric.html @@ -0,0 +1,26 @@ + + + + +

{{ field.dimtitle }}

+ + +
+ + +
+ + + + +

+

+
+ +
+
+
+
+
diff --git a/src/addons/mod/workshop/assessment/rubric/component/rubric.ts b/src/addons/mod/workshop/assessment/rubric/component/rubric.ts new file mode 100644 index 000000000..bd8de3a09 --- /dev/null +++ b/src/addons/mod/workshop/assessment/rubric/component/rubric.ts @@ -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 { } diff --git a/src/addons/mod/workshop/assessment/rubric/lang.json b/src/addons/mod/workshop/assessment/rubric/lang.json new file mode 100644 index 000000000..8b0b20c4a --- /dev/null +++ b/src/addons/mod/workshop/assessment/rubric/lang.json @@ -0,0 +1,4 @@ +{ + "dimensionnumber": "Criterion {{$a}}", + "mustchooseone": "You have to select one of these items" +} \ No newline at end of file diff --git a/src/addons/mod/workshop/assessment/rubric/rubric.module.ts b/src/addons/mod/workshop/assessment/rubric/rubric.module.ts new file mode 100644 index 000000000..04885352e --- /dev/null +++ b/src/addons/mod/workshop/assessment/rubric/rubric.module.ts @@ -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 {} diff --git a/src/addons/mod/workshop/assessment/rubric/services/handler.ts b/src/addons/mod/workshop/assessment/rubric/services/handler.ts new file mode 100644 index 000000000..a4bbd1d91 --- /dev/null +++ b/src/addons/mod/workshop/assessment/rubric/services/handler.ts @@ -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 { + return true; + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return AddonModWorkshopAssessmentStrategyRubricComponent; + } + + /** + * @inheritdoc + */ + async getOriginalValues( + form: AddonModWorkshopGetAssessmentFormDefinitionData, + ): Promise { + 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 { + 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); diff --git a/src/addons/mod/workshop/classes/assessment-strategy-component.ts b/src/addons/mod/workshop/classes/assessment-strategy-component.ts new file mode 100644 index 000000000..1e9c85548 --- /dev/null +++ b/src/addons/mod/workshop/classes/assessment-strategy-component.ts @@ -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; + @Input() strategy!: string; + @Input() moduleId!: number; + @Input() courseId?: number; + +} diff --git a/src/addons/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html b/src/addons/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html new file mode 100644 index 000000000..d4da19737 --- /dev/null +++ b/src/addons/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html @@ -0,0 +1,68 @@ +

{{ 'addon.mod_workshop.assessmentform' | translate }}

+ +
+ + + + + + + + +

{{ 'addon.mod_workshop.assessmentstrategynotsupported' | translate:{$a: strategy} }}

+
+
+
+ + + +

{{ 'addon.mod_workshop.overallfeedback' | translate }}

+
+ + + + {{ 'addon.mod_workshop.feedbackauthor' | translate }} + + + + + + + + + + + + + {{ 'addon.mod_workshop.assessmentweight' | translate }} + + + + {{w}} + + + + + + + + + + + + + +
+
+
diff --git a/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts b/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts new file mode 100644 index 000000000..f46b85d90 --- /dev/null +++ b/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts @@ -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; + 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 { + 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 { + 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 = offlineData.feedbackauthor; + + if (this.access.canallocate) { + this.data.assessment.weight = 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( + 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 { + 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; + 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[] = []; + + // 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; diff --git a/src/addons/mod/workshop/components/assessment/addon-mod-workshop-assessment.html b/src/addons/mod/workshop/components/assessment/addon-mod-workshop-assessment.html new file mode 100644 index 000000000..6439a8eaa --- /dev/null +++ b/src/addons/mod/workshop/components/assessment/addon-mod-workshop-assessment.html @@ -0,0 +1,27 @@ + + + + +

{{profile.fullname}}

+

+ {{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}} +

+

+ {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}} +

+

+ {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggradeover}} +

+

+ {{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }} +

+ {{ 'addon.mod_workshop.notassessed' | translate }} + + {{ 'addon.mod_workshop.assess' | translate }} + +
+ + {{ 'core.notsent' | translate }} + +
+
diff --git a/src/addons/mod/workshop/components/assessment/assessment.ts b/src/addons/mod/workshop/components/assessment/assessment.ts new file mode 100644 index 000000000..42fc54af1 --- /dev/null +++ b/src/addons/mod/workshop/components/assessment/assessment.ts @@ -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[] = []; + + 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; + if (userId == this.currentUserId) { + assessOffline = AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId) .then((offlineAssess) => { + this.offline = true; + this.assessment.weight = 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 = 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 { + 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); + + } + +} diff --git a/src/addons/mod/workshop/pages/assessment/assessment.html b/src/addons/mod/workshop/pages/assessment/assessment.html new file mode 100644 index 000000000..3e6f44b5b --- /dev/null +++ b/src/addons/mod/workshop/pages/assessment/assessment.html @@ -0,0 +1,107 @@ + + + + + + + + + + + + {{ 'core.save' | translate }} + + + + + + + + + + + + + + + +

{{profile.fullname}}

+ +

+ {{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}} +

+

+ {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}} +

+

+ {{ 'addon.mod_workshop.gradinggradeover' | translate }}: {{assessment.gradinggradeover}} +

+

+ {{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }} +

+ + {{ 'addon.mod_workshop.notassessed' | translate }} + +
+
+ + + + +
+ +

{{ 'addon.mod_workshop.assessmentsettings' | translate }}

+
+ + + + {{ 'addon.mod_workshop.assessmentweight' | translate }} + + + + {{ w }} + + + + +

{{ 'addon.mod_workshop.gradinggradecalculated' | translate }}

+

{{ assessment.gradinggrade }}

+
+
+ + {{ 'addon.mod_workshop.gradinggradeover' | translate }} + + + {{grade.label}} + + + + + {{ 'addon.mod_workshop.feedbackreviewer' | translate }} + + + +
+ + + + +

+ {{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }} +

+ + +
+
+
+
+
diff --git a/src/addons/mod/workshop/pages/assessment/assessment.ts b/src/addons/mod/workshop/pages/assessment/assessment.ts new file mode 100644 index 000000000..2085832e1 --- /dev/null +++ b/src/addons/mod/workshop/pages/assessment/assessment.ts @@ -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('assessment')!; + this.submission = CoreNavigator.getRouteParam('submission')!; + this.profile = CoreNavigator.getRouteParam('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 { + 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 { + 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 { + const promises: Promise[] = []; + + 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 { + // 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 { + 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; +}; diff --git a/src/addons/mod/workshop/pages/edit-submission/edit-submission.html b/src/addons/mod/workshop/pages/edit-submission/edit-submission.html new file mode 100644 index 000000000..b1a771b6c --- /dev/null +++ b/src/addons/mod/workshop/pages/edit-submission/edit-submission.html @@ -0,0 +1,46 @@ + + + + + + {{ 'addon.mod_workshop.editsubmission' | translate }} + + + {{ 'core.save' | translate }} + + + + + + +
+ + + + {{ 'addon.mod_workshop.submissiontitle' | translate }} + + + + + + + + + + {{ 'addon.mod_workshop.submissioncontent' | translate }} + + + + + + + +
+
+
diff --git a/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts new file mode 100644 index 000000000..ffa567c29 --- /dev/null +++ b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts @@ -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 = {}; // 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('module')!; + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.access = CoreNavigator.getRouteParam('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 { + 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 { + 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 { + // 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 { + 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[]; +}; diff --git a/src/addons/mod/workshop/pages/submission/submission.html b/src/addons/mod/workshop/pages/submission/submission.html new file mode 100644 index 000000000..557fb0dc7 --- /dev/null +++ b/src/addons/mod/workshop/pages/submission/submission.html @@ -0,0 +1,154 @@ + + + + + + + + + + + + {{ 'core.save' | translate }} + + + {{ 'core.save' | translate }} + + + + + + + + + + + + + + + + + {{ 'addon.mod_workshop.editsubmission' | translate }} + + + + {{ 'addon.mod_workshop.deletesubmission' | translate }} + + + + {{ 'core.restore' | translate }} + + + + + + + + + +

+ {{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }} +

+ + +
+
+
+ + + + +

{{ 'addon.mod_workshop.yourassessment' | translate }}

+
+
+ + +
+ + + + +

{{ 'addon.mod_workshop.receivedgrades' | translate }}

+
+
+ + + + +
+ + + + +

{{ 'addon.mod_workshop.givengrades' | translate }}

+
+
+ + +
+ +
+ + +

{{ 'addon.mod_workshop.feedbackauthor' | translate }}

+
+
+ + {{ 'addon.mod_workshop.publishsubmission' | translate }} + +

{{ 'addon.mod_workshop.publishsubmission_help' | translate }}

+
+ + + +

{{ 'addon.mod_workshop.gradecalculated' | translate }}

+

{{ submission.grade }}

+
+
+ + {{ 'addon.mod_workshop.gradeover' | translate }} + + + {{grade.label}} + + + + + {{ 'addon.mod_workshop.feedbackauthor' | translate }} + + + +
+ + + + + + + + +

+ {{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateGradingByProfile.fullname} }} +

+ + +
+
+
+
+
diff --git a/src/addons/mod/workshop/pages/submission/submission.ts b/src/addons/mod/workshop/pages/submission/submission.ts new file mode 100644 index 000000000..b415c04f5 --- /dev/null +++ b/src/addons/mod/workshop/pages/submission/submission.ts @@ -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 & { 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 { + + this.submissionId = CoreNavigator.getRouteNumberParam('submissionId')!; + this.module = CoreNavigator.getRouteParam('module')!; + this.workshop = CoreNavigator.getRouteParam('workshop')!; + this.access = CoreNavigator.getRouteParam('access')!; + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.profile = CoreNavigator.getRouteParam('profile'); + this.submissionInfo = CoreNavigator.getRouteParam('submission')!; + this.assessment = CoreNavigator.getRouteParam('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 { + 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 { + try { + this.submission = await AddonModWorkshopHelper.getSubmissionById(this.workshopId, this.submissionId, { + cmId: this.module.id, + }); + + const promises: Promise[] = []; + + 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 { + const promises: Promise[] = []; + + 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 { + 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 { + // 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 { + 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 { + 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 { + 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; +}; diff --git a/src/addons/mod/workshop/services/assessment-strategy-delegate.ts b/src/addons/mod/workshop/services/assessment-strategy-delegate.ts new file mode 100644 index 000000000..b3ad2256d --- /dev/null +++ b/src/addons/mod/workshop/services/assessment-strategy-delegate.ts @@ -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; + + /** + * 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; + + /** + * 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>; +} + +/** + * 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 { + + 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 | 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 { + 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 | undefined> { + return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'prepareAssessmentData', [currentValues, form])); + } + +} +export const AddonWorkshopAssessmentStrategyDelegate = makeSingleton(AddonWorkshopAssessmentStrategyDelegateService);