MOBILE-3657 workshop: Submission and assessments

main
Pau Ferrer Ocaña 2021-04-23 13:25:19 +02:00
parent 8db22cc54a
commit b037c96b0b
34 changed files with 3695 additions and 1 deletions

View File

@ -3,7 +3,7 @@
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<core-rich-text-editor *ngIf="editMode" [control]="form.controls['f_'+field.id]" [placeholder]="field.name"
[formControlName]="'f_'+field.id" [component]="component" [componentId]="componentId" [autoSave]="true"
[name]="'f_'+field.id" [component]="component" [componentId]="componentId" [autoSave]="true"
contextLevel="module" [contextInstanceId]="componentId" [elementId]="'field_'+field.id" ngDefaultControl>
</core-rich-text-editor>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>

View File

@ -0,0 +1,44 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { 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 {}

View File

@ -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 { }

View File

@ -0,0 +1,50 @@
<ng-container *ngFor="let field of assessment.form?.fields; let n = index">
<ion-card *ngIf="n < assessment.form?.dimenssionscount">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ field.dimtitle }}</h2>
<core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item *ngIf="edit && field.grades">
<ion-label position="stacked">
<span [core-mark-required]="true">
{{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }}
</span>
</ion-label>
<ion-select [(ngModel)]="selectedValues[n].grade" interface="action-sheet">
<ion-select-option *ngFor="let grade of field.grades" [value]="grade.value">{{grade.label}}</ion-select-option>
</ion-select>
<core-input-errors *ngIf="fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]">
</core-input-errors>
</ion-item>
<ion-item *ngIf="!edit && field.grades" class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }}</h2>
<ng-container *ngFor="let grade of field.grades">
<p *ngIf="grade.value === selectedValues[n].grade">{{grade.label}}</p>
</ng-container>
</ion-label>
</ion-item>
<ion-item *ngIf="edit">
<ion-label position="stacked">
{{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
</ion-label>
<ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" core-auto-rows></ion-textarea>
</ion-item>
<ion-item *ngIf="!edit" class="ion-text-wrap">
<ion-label>
<h2>
{{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
</h2>
<p>
<core-format-text [text]="selectedValues[n].peercomment" contextLevel="module" [contextInstanceId]="moduleId"
[courseId]="courseId">
</core-format-text>
</p>
</ion-label>
</ion-item>
</ion-card>
</ng-container>

View File

@ -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"
}

View File

@ -0,0 +1,151 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
AddonModWorkshopAssessmentStrategyFieldErrors,
} from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy';
import {
AddonModWorkshopGetAssessmentFormDefinitionData,
AddonModWorkshopGetAssessmentFormFieldsParsedData,
} from '@addons/mod/workshop/services/workshop';
import { Injectable, Type } from '@angular/core';
import { CoreGradesHelper } from '@features/grades/services/grades-helper';
import { makeSingleton, Translate } from '@singletons';
import { CoreFormFields } from '@singletons/form';
import { AddonWorkshopAssessmentStrategyHandler } from '../../../services/assessment-strategy-delegate';
import { AddonModWorkshopAssessmentStrategyAccumulativeComponent } from '../component/accumulative';
/**
* Handler for accumulative assessment strategy plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModWorkshopAssessmentStrategyAccumulativeHandlerService implements AddonWorkshopAssessmentStrategyHandler {
name = 'AddonModWorkshopAssessmentStrategyAccumulative';
strategyName = 'accumulative';
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
getComponent(): Type<unknown> {
return AddonModWorkshopAssessmentStrategyAccumulativeComponent;
}
/**
* @inheritdoc
*/
async getOriginalValues(
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
const defaultGrade = Translate.instant('core.choosedots');
const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = [];
const promises: Promise<void>[] = [];
form.fields.forEach((field, n) => {
field.dimtitle = Translate.instant('addon.mod_workshop_assessment_accumulative.dimensionnumber', { $a: field.number });
if (!form.current[n]) {
form.current[n] = {};
}
originalValues[n] = {};
originalValues[n].peercomment = form.current[n].peercomment || '';
originalValues[n].number = field.number; // eslint-disable-line id-blacklist
form.current[n].grade = form.current[n].grade ? parseInt(String(form.current[n].grade), 10) : -1;
const gradingType = parseInt(String(field.grade), 10);
const dimension = form.dimensionsinfo.find((dimension) => dimension.id == parseInt(field.dimensionid, 10));
const scale = dimension && gradingType < 0 ? dimension.scale : undefined;
promises.push(CoreGradesHelper.makeGradesMenu(gradingType, undefined, defaultGrade, -1, scale).then((grades) => {
field.grades = grades;
originalValues[n].grade = form.current[n].grade;
return;
}));
});
await Promise.all(promises);
return originalValues;
}
/**
* @inheritdoc
*/
hasDataChanged(
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
): boolean {
for (const x in originalValues) {
if (originalValues[x].grade != currentValues[x].grade) {
return true;
}
if (originalValues[x].peercomment != currentValues[x].peercomment) {
return true;
}
}
return false;
}
/**
* @inheritdoc
*/
async prepareAssessmentData(
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<CoreFormFields> {
const data: CoreFormFields = {};
const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {};
let hasErrors = false;
form.fields.forEach((field, idx) => {
if (idx < form.dimenssionscount) {
const grade = parseInt(String(currentValues[idx].grade), 10);
if (!isNaN(grade) && grade >= 0) {
data['grade__idx_' + idx] = grade;
} else {
errors['grade_' + idx] = Translate.instant('addon.mod_workshop_assessment_accumulative.mustchoosegrade');
hasErrors = true;
}
if (currentValues[idx].peercomment) {
data['peercomment__idx_' + idx] = currentValues[idx].peercomment;
}
data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0;
data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10);
data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0;
}
});
if (hasErrors) {
throw errors;
}
return data;
}
}
export const AddonModWorkshopAssessmentStrategyAccumulativeHandler =
makeSingleton(AddonModWorkshopAssessmentStrategyAccumulativeHandlerService);

View File

@ -0,0 +1,29 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { AddonModWorkshopAssessmentStrategyAccumulativeModule } from './accumulative/accumulative.module';
import { AddonModWorkshopAssessmentStrategyCommentsModule } from './comments/comments.module';
import { AddonModWorkshopAssessmentStrategyNumErrorsModule } from './numerrors/numerrors.module';
import { AddonModWorkshopAssessmentStrategyRubricModule } from './rubric/rubric.module';
@NgModule({
imports: [
AddonModWorkshopAssessmentStrategyAccumulativeModule,
AddonModWorkshopAssessmentStrategyCommentsModule,
AddonModWorkshopAssessmentStrategyNumErrorsModule,
AddonModWorkshopAssessmentStrategyRubricModule,
],
})
export class AddonModWorkshopAssessmentStrategyModule {}

View File

@ -0,0 +1,44 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSharedModule } from '@/core/shared.module';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import { 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 {}

View File

@ -0,0 +1,30 @@
<ng-container *ngFor="let field of assessment.form?.fields; let n = index">
<ion-card *ngIf="n < assessment.form?.dimenssionscount">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ field.dimtitle }}</h2>
<core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item *ngIf="edit">
<ion-label position="stacked">
<span [core-mark-required]="true">
{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
</span>
</ion-label>
<ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" core-auto-rows></ion-textarea>
<core-input-errors *ngIf="fieldErrors['peercomment_' + n]" [errorText]="fieldErrors['peercomment_' + n]">
</core-input-errors>
</ion-item>
<ion-item *ngIf="!edit" class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</h2>
<p><core-format-text [text]="selectedValues[n].peercomment" contextLevel="module" [contextInstanceId]="moduleId"
[courseId]="courseId">
</core-format-text></p>
</ion-label>
</ion-item>
</ion-card>
</ng-container>

View File

@ -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 { }

View File

@ -0,0 +1,4 @@
{
"dimensioncommentfor": "Comment for {{$a}}",
"dimensionnumber": "Aspect {{$a}}"
}

View File

@ -0,0 +1,124 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
AddonModWorkshopAssessmentStrategyFieldErrors,
} from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy';
import { AddonWorkshopAssessmentStrategyHandler } from '@addons/mod/workshop/services/assessment-strategy-delegate';
import {
AddonModWorkshopGetAssessmentFormDefinitionData,
AddonModWorkshopGetAssessmentFormFieldsParsedData,
} from '@addons/mod/workshop/services/workshop';
import { Injectable, Type } from '@angular/core';
import { makeSingleton, Translate } from '@singletons';
import { CoreFormFields } from '@singletons/form';
import { AddonModWorkshopAssessmentStrategyCommentsComponent } from '../component/comments';
/**
* Handler for comments assessment strategy plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModWorkshopAssessmentStrategyCommentsHandlerService implements AddonWorkshopAssessmentStrategyHandler {
name = 'AddonModWorkshopAssessmentStrategyComments';
strategyName = 'comments';
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
getComponent(): Type<unknown> {
return AddonModWorkshopAssessmentStrategyCommentsComponent;
}
/**
* @inheritdoc
*/
async getOriginalValues(
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = [];
form.fields.forEach((field, n) => {
field.dimtitle = Translate.instant('addon.mod_workshop_assessment_comments.dimensionnumber', { $a: field.number });
if (!form.current[n]) {
form.current[n] = {};
}
originalValues[n] = {};
originalValues[n].peercomment = form.current[n].peercomment || '';
originalValues[n].number = field.number; // eslint-disable-line id-blacklist
});
return originalValues;
}
/**
* @inheritdoc
*/
hasDataChanged(
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
): boolean {
for (const x in originalValues) {
if (originalValues[x].peercomment != currentValues[x].peercomment) {
return true;
}
}
return false;
}
/**
* @inheritdoc
*/
async prepareAssessmentData(
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<CoreFormFields> {
const data: CoreFormFields = {};
const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {};
let hasErrors = false;
form.fields.forEach((field, idx) => {
if (idx < form.dimenssionscount) {
if (currentValues[idx].peercomment) {
data['peercomment__idx_' + idx] = currentValues[idx].peercomment;
} else {
errors['peercomment_' + idx] = Translate.instant('core.err_required');
hasErrors = true;
}
data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0;
data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10);
}
});
if (hasErrors) {
throw errors;
}
return data;
}
}
export const AddonModWorkshopAssessmentStrategyCommentsHandler =
makeSingleton(AddonModWorkshopAssessmentStrategyCommentsHandlerService);

View File

@ -0,0 +1,53 @@
<ng-container *ngFor="let field of assessment.form?.fields; let n = index">
<ion-card *ngIf="n < assessment.form?.dimenssionscount">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ field.dimtitle }}</h2>
<core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<ion-list>
<ion-radio-group [(ngModel)]="selectedValues[n].grade" [name]="'grade_' + n">
<ion-item>
<ion-label position="stacked">
<span [core-mark-required]="edit">
{{ 'addon.mod_workshop.yourassessmentfor' | translate : {'$a': field.dimtitle } }}
</span>
</ion-label>
<core-input-errors *ngIf="edit && fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]">
</core-input-errors>
</ion-item>
<ion-item>
<ion-label>
<core-format-text [text]="field.grade0" [filter]="false"></core-format-text>
</ion-label>
<ion-radio slot="start" [value]="-1" [disabled]="!edit"></ion-radio>
</ion-item>
<ion-item>
<ion-label><core-format-text [text]="field.grade1" [filter]="false"></core-format-text></ion-label>
<ion-radio slot="start" [value]="1" [disabled]="!edit"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
<ion-item *ngIf="edit">
<ion-label position="stacked">
{{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
</ion-label>
<ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" [name]="'peercomment_' + n"
core-auto-rows>
</ion-textarea>
</ion-item>
<ion-item *ngIf="!edit" class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</h2>
<p>
<core-format-text [text]="selectedValues[n].peercomment" contextLevel="module" [contextInstanceId]="moduleId"
[courseId]="courseId">
</core-format-text>
</p>
</ion-label>
</ion-item>
</ion-card>
</ng-container>

View File

@ -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 { }

View File

@ -0,0 +1,5 @@
{
"dimensioncommentfor": "Comment for {{$a}}",
"dimensiongradefor": "Grade for {{$a}}",
"dimensionnumber": "Assertion {{$a}}"
}

View File

@ -0,0 +1,44 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSharedModule } from '@/core/shared.module';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import { 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 {}

View File

@ -0,0 +1,134 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
AddonModWorkshopAssessmentStrategyFieldErrors,
} from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy';
import { AddonWorkshopAssessmentStrategyHandler } from '@addons/mod/workshop/services/assessment-strategy-delegate';
import {
AddonModWorkshopGetAssessmentFormDefinitionData,
AddonModWorkshopGetAssessmentFormFieldsParsedData,
} from '@addons/mod/workshop/services/workshop';
import { Injectable, Type } from '@angular/core';
import { Translate, makeSingleton } from '@singletons';
import { CoreFormFields } from '@singletons/form';
import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from '../component/numerrors';
/**
* Handler for numerrors assessment strategy plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModWorkshopAssessmentStrategyNumErrorsHandlerService implements AddonWorkshopAssessmentStrategyHandler {
name = 'AddonModWorkshopAssessmentStrategyNumErrors';
strategyName = 'numerrors';
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
getComponent(): Type<unknown> {
return AddonModWorkshopAssessmentStrategyNumErrorsComponent;
}
/**
* @inheritdoc
*/
async getOriginalValues(
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = [];
form.fields.forEach((field, n) => {
field.dimtitle = Translate.instant('addon.mod_workshop_assessment_numerrors.dimensionnumber', { $a: field.number });
if (!form.current[n]) {
form.current[n] = {};
}
originalValues[n] = {};
originalValues[n].peercomment = form.current[n].peercomment || '';
originalValues[n].number = field.number; // eslint-disable-line id-blacklist
originalValues[n].grade = form.current[n].grade || '';
});
return originalValues;
}
/**
* @inheritdoc
*/
hasDataChanged(
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
): boolean {
for (const x in originalValues) {
if (originalValues[x].grade != currentValues[x].grade) {
return true;
}
if (originalValues[x].peercomment != currentValues[x].peercomment) {
return true;
}
}
return false;
}
/**
* @inheritdoc
*/
async prepareAssessmentData(
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<CoreFormFields> {
const data: CoreFormFields = {};
const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {};
let hasErrors = false;
form.fields.forEach((field, idx) => {
if (idx < form.dimenssionscount) {
const grade = parseInt(String(currentValues[idx].grade), 10);
if (!isNaN(grade) && (grade == 1 || grade == -1)) {
data['grade__idx_' + idx] = grade;
} else {
errors['grade_' + idx] = Translate.instant('core.required');
hasErrors = true;
}
if (currentValues[idx].peercomment) {
data['peercomment__idx_' + idx] = currentValues[idx].peercomment;
}
data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0;
data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10);
data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0;
}
});
if (hasErrors) {
throw errors;
}
return data;
}
}
export const AddonModWorkshopAssessmentStrategyNumErrorsHandler =
makeSingleton(AddonModWorkshopAssessmentStrategyNumErrorsHandlerService);

View File

@ -0,0 +1,26 @@
<ng-container *ngFor="let field of assessment.form?.fields; let n = index">
<ion-card *ngIf="n < assessment.form?.dimenssionscount">
<ion-item class="ion-text-wrap">
<ion-label>
<h2 [core-mark-required]="edit">{{ field.dimtitle }}</h2>
<core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId"
[courseId]="courseId">
</core-format-text>
</ion-label>
<core-input-errors *ngIf="edit && fieldErrors['chosenlevelid_' + n]" [errorText]="fieldErrors['chosenlevelid_' + n]">
</core-input-errors>
</ion-item>
<ion-list>
<ion-radio-group [(ngModel)]="selectedValues[n].chosenlevelid" [name]="'chosenlevelid_' + n">
<ion-item *ngFor="let subfield of field.fields">
<ion-label>
<p><core-format-text [text]="subfield.definition" contextLevel="module"
[contextInstanceId]="moduleId" [courseId]="courseId">
</core-format-text></p>
</ion-label>
<ion-radio slot="start" [value]="subfield.levelid" [disabled]="!edit"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
</ion-card>
</ng-container>

View File

@ -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 { }

View File

@ -0,0 +1,4 @@
{
"dimensionnumber": "Criterion {{$a}}",
"mustchooseone": "You have to select one of these items"
}

View File

@ -0,0 +1,44 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSharedModule } from '@/core/shared.module';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import { AddonModWorkshopAssessmentStrategyRubricComponent } from './component/rubric';
import { AddonModWorkshopAssessmentStrategyRubricHandler } from './services/handler';
@NgModule({
declarations: [
AddonModWorkshopAssessmentStrategyRubricComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonWorkshopAssessmentStrategyDelegate.registerHandler(
AddonModWorkshopAssessmentStrategyRubricHandler.instance,
);
},
},
],
exports: [
AddonModWorkshopAssessmentStrategyRubricComponent,
],
})
export class AddonModWorkshopAssessmentStrategyRubricModule {}

View File

@ -0,0 +1,125 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
AddonModWorkshopAssessmentStrategyFieldErrors,
} from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy';
import { AddonWorkshopAssessmentStrategyHandler } from '@addons/mod/workshop/services/assessment-strategy-delegate';
import {
AddonModWorkshopGetAssessmentFormDefinitionData,
AddonModWorkshopGetAssessmentFormFieldsParsedData,
} from '@addons/mod/workshop/services/workshop';
import { Injectable, Type } from '@angular/core';
import { Translate, makeSingleton } from '@singletons';
import { CoreFormFields } from '@singletons/form';
import { AddonModWorkshopAssessmentStrategyRubricComponent } from '../component/rubric';
/**
* Handler for rubric assessment strategy plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModWorkshopAssessmentStrategyRubricHandlerService implements AddonWorkshopAssessmentStrategyHandler {
name = 'AddonModWorkshopAssessmentStrategyRubric';
strategyName = 'rubric';
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
getComponent(): Type<unknown> {
return AddonModWorkshopAssessmentStrategyRubricComponent;
}
/**
* @inheritdoc
*/
async getOriginalValues(
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = [];
form.fields.forEach((field, n) => {
field.dimtitle = Translate.instant('addon.mod_workshop_assessment_rubric.dimensionnumber', { $a: field.number });
if (!form.current[n]) {
form.current[n] = {};
}
originalValues[n] = {};
originalValues[n].chosenlevelid = form.current[n].chosenlevelid || '';
originalValues[n].number = field.number; // eslint-disable-line id-blacklist
});
return originalValues;
}
/**
* @inheritdoc
*/
hasDataChanged(
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
): boolean {
for (const x in originalValues) {
if (originalValues[x].chosenlevelid != (currentValues[x].chosenlevelid || '')) {
return true;
}
}
return false;
}
/**
* @inheritdoc
*/
async prepareAssessmentData(
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<CoreFormFields> {
const data: CoreFormFields = {};
const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {};
let hasErrors = false;
form.fields.forEach((field, idx) => {
if (idx < form.dimenssionscount) {
const id = parseInt(currentValues[idx].chosenlevelid, 10);
if (!isNaN(id) && id >= 0) {
data['chosenlevelid__idx_' + idx] = id;
} else {
errors['chosenlevelid_' + idx] = Translate.instant('addon.mod_workshop_assessment_rubric.mustchooseone');
hasErrors = true;
}
data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0;
data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10);
}
});
if (hasErrors) {
throw errors;
}
return data;
}
}
export const AddonModWorkshopAssessmentStrategyRubricHandler =
makeSingleton(AddonModWorkshopAssessmentStrategyRubricHandlerService);

View File

@ -0,0 +1,36 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input } from '@angular/core';
import { AddonModWorkshopGetAssessmentFormFieldsParsedData } from '../services/workshop';
import { AddonModWorkshopSubmissionAssessmentWithFormData } from '../services/workshop-helper';
/**
* Base class for component to render an assessment strategy.
*/
@Component({
template: '',
})
export class AddonModWorkshopAssessmentStrategyBaseComponent {
@Input() workshopId!: number;
@Input() assessment!: AddonModWorkshopSubmissionAssessmentWithFormData;
@Input() edit!: boolean;
@Input() selectedValues!: AddonModWorkshopGetAssessmentFormFieldsParsedData[];
@Input() fieldErrors!: Record<string, string>;
@Input() strategy!: string;
@Input() moduleId!: number;
@Input() courseId?: number;
}

View File

@ -0,0 +1,68 @@
<h3 class="ion-padding">{{ 'addon.mod_workshop.assessmentform' | translate }}</h3>
<form name="mma-mod_workshop-assessment-form" #assessmentForm>
<core-loading [hideUntil]="assessmentStrategyLoaded">
<ng-container *ngIf="componentClass && assessmentStrategyLoaded">
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
</ng-container>
<ion-card class="core-info-card" *ngIf="notSupported">
<ion-item>
<ion-label>
<p>{{ 'addon.mod_workshop.assessmentstrategynotsupported' | translate:{$a: strategy} }}</p>
</ion-label>
</ion-item>
</ion-card>
<ion-card *ngIf="assessmentStrategyLoaded && overallFeedkback &&
(edit || data.assessment?.feedbackauthor || data.assessment?.feedbackattachmentfiles?.length) ">
<ion-item class="ion-text-wrap">
<ion-label><h2>{{ 'addon.mod_workshop.overallfeedback' | translate }}</h2></ion-label>
</ion-item>
<ion-item position="stacked" *ngIf="edit">
<ion-label position="stacked">
<span [core-mark-required]="overallFeedkbackRequired">
{{ 'addon.mod_workshop.feedbackauthor' | translate }}
</span>
</ion-label>
<core-rich-text-editor [control]="feedbackControl" [component]="component"
[componentId]="workshop.coursemodule" [autoSave]="true" contextLevel="module"
[contextInstanceId]="workshop.coursemodule" elementId="feedbackauthor_editor"
[draftExtraParams]="{asid: assessmentId}" (contentChanged)="onFeedbackChange($event)">
</core-rich-text-editor>
<core-input-errors
*ngIf="overallFeedkbackRequired && data.fieldErrors && data.fieldErrors['feedbackauthor']"
[errorText]="data.fieldErrors['feedbackauthor']">
</core-input-errors>
</ion-item>
<core-attachments *ngIf="edit && workshop.overallfeedbackfiles" [files]="data.assessment?.feedbackattachmentfiles"
[maxSize]="workshop.overallfeedbackmaxbytes" [maxSubmissions]="workshop.overallfeedbackfiles"
[component]="component" [componentId]="componentId" [allowOffline]="true">
</core-attachments>
<ion-item *ngIf="edit && access && access.canallocate">
<ion-label position="stacked">
<span [core-mark-required]="true">
{{ 'addon.mod_workshop.assessmentweight' | translate }}
</span>
</ion-label>
<ion-select [(ngModel)]="weight" interface="action-sheet" name="weight">
<ion-select-option *ngFor="let w of weights" [value]="w">{{w}}</ion-select-option>
</ion-select>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="!edit && data.assessment?.feedbackauthor">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="data.assessment?.feedbackauthor"
contextLevel="module" [contextInstanceId]="workshop.coursemodule" [courseId]="workshop.course">
</core-format-text>
</ion-label>
</ion-item>
<ion-item *ngIf="!edit && workshop.overallfeedbackfiles && data.assessment?.feedbackattachmentfiles?.length"
lines="none">
<ion-label>
<core-files [files]="data.assessment?.feedbackattachmentfiles" [component]="component"
[componentId]="componentId"></core-files>
</ion-label>
</ion-item>
</ion-card>
</core-loading>
</form>

View File

@ -0,0 +1,426 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit, ViewChild, ElementRef, Type, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';
import { CoreError } from '@classes/errors/error';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CoreFile } from '@services/file';
import { CoreFileEntry, CoreFileHelper } from '@services/file-helper';
import { CoreFileSession } from '@services/file-session';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreFormFields, CoreForms } from '@singletons/form';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import {
AddonModWorkshopProvider,
AddonModWorkshopOverallFeedbackMode,
AddonModWorkshop,
AddonModWorkshopData,
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
AddonModWorkshopGetAssessmentFormFieldsParsedData,
} from '../../services/workshop';
import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper';
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
/**
* Component that displays workshop assessment strategy form.
*/
@Component({
selector: 'addon-mod-workshop-assessment-strategy',
templateUrl: 'addon-mod-workshop-assessment-strategy.html',
})
export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDestroy {
@Input() workshop!: AddonModWorkshopData;
@Input() access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
@Input() assessmentId!: number;
@Input() userId!: number;
@Input() strategy!: string;
@Input() edit = false;
@ViewChild('assessmentForm') formElement!: ElementRef;
componentClass?: Type<unknown>;
data: AddonModWorkshopAssessmentStrategyData = {
workshopId: 0,
assessment: undefined,
edit: false,
selectedValues: [],
fieldErrors: {},
strategy: '',
moduleId: 0,
courseId: undefined,
};
assessmentStrategyLoaded = false;
notSupported = false;
feedbackText = '';
feedbackControl = new FormControl();
overallFeedkback = false;
overallFeedkbackRequired = false;
component = AddonModWorkshopProvider.COMPONENT;
componentId?: number;
weights: number[] = [];
weight?: number;
protected obsInvalidated?: CoreEventObserver;
protected hasOffline = false;
protected originalData: {
text: string;
files: CoreFileEntry[];
weight: number;
selectedValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[];
} = {
text: '',
files: [],
weight: 1,
selectedValues: [],
};
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
if (!this.assessmentId || !this.strategy) {
this.assessmentStrategyLoaded = true;
return;
}
this.data.workshopId = this.workshop.id;
this.data.edit = this.edit;
this.data.strategy = this.strategy;
this.data.moduleId = this.workshop.coursemodule;
this.data.courseId = this.workshop.course;
this.componentClass = AddonWorkshopAssessmentStrategyDelegate.getComponentForPlugin(this.strategy);
if (this.componentClass) {
this.overallFeedkback = this.workshop.overallfeedbackmode != AddonModWorkshopOverallFeedbackMode.DISABLED;
this.overallFeedkbackRequired =
this.workshop.overallfeedbackmode == AddonModWorkshopOverallFeedbackMode.ENABLED_REQUIRED;
this.componentId = this.workshop.coursemodule;
// Load Weights selector.
if (this.edit && this.access.canallocate) {
this.weights;
for (let i = 16; i >= 0; i--) {
this.weights[i] = i;
}
}
// Check if rich text editor is enabled.
if (this.edit) {
// Block the workshop.
CoreSync.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshop.id);
}
try {
await this.load();
this.obsInvalidated = CoreEvents.on(
AddonModWorkshopProvider.ASSESSMENT_INVALIDATED,
this.load.bind(this),
CoreSites.getCurrentSiteId(),
);
} catch (error) {
this.componentClass = undefined;
CoreDomUtils.showErrorModalDefault(error, 'Error loading assessment.');
} finally {
this.assessmentStrategyLoaded = true;
}
} else {
// Helper data and fallback.
this.notSupported = !AddonWorkshopAssessmentStrategyDelegate.isPluginSupported(this.strategy);
this.assessmentStrategyLoaded = true;
}
}
/**
* Convenience function to load the assessment data.
*
* @return Promised resvoled when data is loaded.
*/
protected async load(): Promise<void> {
this.data.assessment = await AddonModWorkshopHelper.getReviewerAssessmentById(this.workshop.id, this.assessmentId, {
userId: this.userId,
cmId: this.workshop.coursemodule,
});
if (this.edit) {
try {
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId);
const offlineData = offlineAssessment.inputdata;
this.hasOffline = true;
this.data.assessment.feedbackauthor = <string>offlineData.feedbackauthor;
if (this.access.canallocate) {
this.data.assessment.weight = <number>offlineData.weight;
}
// Override assessment plugins values.
this.data.assessment.form!.current = AddonModWorkshop.parseFields(
CoreUtils.objectToArrayOfObjects(offlineData, 'name', 'value'),
);
// Override offline files.
if (offlineData) {
this.data.assessment.feedbackattachmentfiles =
await AddonModWorkshopHelper.getAssessmentFilesFromOfflineFilesObject(
<CoreFileUploaderStoreFilesResult>offlineData.feedbackauthorattachmentsid,
this.workshop.id,
this.assessmentId,
);
}
} catch {
this.hasOffline = false;
// Ignore errors.
} finally {
this.feedbackText = this.data.assessment.feedbackauthor;
this.feedbackControl.setValue(this.feedbackText);
this.originalData.text = this.data.assessment.feedbackauthor;
if (this.access.canallocate) {
this.originalData.weight = this.data.assessment.weight;
}
this.originalData.files = [];
this.data.assessment.feedbackattachmentfiles.forEach((file) => {
let filename = CoreFile.getFileName(file);
if (!filename) {
// We don't have filename, extract it from the path.
filename = CoreFileHelper.getFilenameFromPath(file) || '';
}
this.originalData.files.push({
filename,
fileurl: '', // No needed to compare.
});
});
}
}
try {
this.data.selectedValues = await AddonWorkshopAssessmentStrategyDelegate.getOriginalValues(
this.strategy,
this.data.assessment.form!,
this.workshop.id,
);
} finally {
this.originalData.selectedValues = CoreUtils.clone(this.data.selectedValues);
if (this.edit) {
CoreFileSession.setFiles(
AddonModWorkshopProvider.COMPONENT,
this.workshop.id + '_' + this.assessmentId,
this.data.assessment.feedbackattachmentfiles,
);
if (this.access.canallocate) {
this.weight = this.data.assessment.weight;
}
}
}
}
/**
* Check if data has changed.
*
* @return True if data has changed.
*/
hasDataChanged(): boolean {
if (!this.assessmentStrategyLoaded) {
return false;
}
// Compare feedback text.
const text = CoreTextUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []);
if (this.originalData.text != text) {
return true;
}
if (this.access.canallocate && this.originalData.weight != this.weight) {
return true;
}
// Compare feedback files.
const files = CoreFileSession.getFiles(
AddonModWorkshopProvider.COMPONENT,
this.workshop.id + '_' + this.assessmentId,
) || [];
if (CoreFileUploader.areFileListDifferent(files, this.originalData.files)) {
return true;
}
return AddonWorkshopAssessmentStrategyDelegate.hasDataChanged(
this.workshop.strategy!,
this.originalData.selectedValues,
this.data.selectedValues,
);
}
/**
* Save the assessment.
*
* @return Promise resolved when done, rejected if assessment could not be saved.
*/
async saveAssessment(): Promise<void> {
const files = CoreFileSession.getFiles(
AddonModWorkshopProvider.COMPONENT,
this.workshop.id + '_' + this.assessmentId,
) || [];
let saveOffline = false;
let allowOffline = !files.length;
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
this.data.fieldErrors = {};
try {
let attachmentsId: CoreFileUploaderStoreFilesResult | number;
try {
// Upload attachments first if any.
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreAssessmentFiles(
this.workshop.id,
this.assessmentId,
files,
saveOffline,
);
} catch {
// Cannot upload them in online, save them in offline.
saveOffline = true;
allowOffline = true;
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreAssessmentFiles(
this.workshop.id,
this.assessmentId,
files,
saveOffline,
);
}
const text = CoreTextUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []);
let assessmentData: CoreFormFields<unknown>;
try {
assessmentData = await AddonModWorkshopHelper.prepareAssessmentData(
this.workshop,
this.data.selectedValues,
text,
this.data.assessment!.form!,
attachmentsId,
);
} catch (errors) {
this.data.fieldErrors = errors;
throw new CoreError(Translate.instant('core.errorinvalidform'));
}
let gradeUpdated = false;
if (saveOffline) {
// Save assessment in offline.
await AddonModWorkshopOffline.saveAssessment(
this.workshop.id,
this.assessmentId,
this.workshop.course,
assessmentData,
);
gradeUpdated = false;
} else {
// Try to send it to server.
// Don't allow offline if there are attachments since they were uploaded fine.
gradeUpdated = await AddonModWorkshop.updateAssessment(
this.workshop.id,
this.assessmentId,
this.workshop.course,
assessmentData,
undefined,
allowOffline,
);
}
CoreForms.triggerFormSubmittedEvent(this.formElement, !!gradeUpdated, CoreSites.getCurrentSiteId());
const promises: Promise<void>[] = [];
// If sent to the server, invalidate and clean.
if (gradeUpdated) {
promises.push(AddonModWorkshopHelper.deleteAssessmentStoredFiles(this.workshop.id, this.assessmentId));
promises.push(AddonModWorkshop.invalidateAssessmentFormData(this.workshop.id, this.assessmentId));
promises.push(AddonModWorkshop.invalidateAssessmentData(this.workshop.id, this.assessmentId));
}
await CoreUtils.ignoreErrors(Promise.all(promises));
CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, {
workshopId: this.workshop.id,
assessmentId: this.assessmentId,
userId: CoreSites.getCurrentSiteUserId(),
}, CoreSites.getCurrentSiteId());
if (files) {
// Delete the local files from the tmp folder.
CoreFileUploader.clearTmpFiles(files);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error saving assessment.');
} finally {
modal.dismiss();
}
}
/**
* Feedback text changed.
*
* @param text The new text.
*/
onFeedbackChange(text: string): void {
this.feedbackText = text;
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.obsInvalidated?.off();
if (this.data.assessment?.feedbackattachmentfiles) {
// Delete the local files from the tmp folder.
CoreFileUploader.clearTmpFiles(this.data.assessment.feedbackattachmentfiles);
}
}
}
type AddonModWorkshopAssessmentStrategyData = {
workshopId: number;
assessment?: AddonModWorkshopSubmissionAssessmentWithFormData;
edit: boolean;
selectedValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[];
fieldErrors: AddonModWorkshopAssessmentStrategyFieldErrors;
strategy: string;
moduleId: number;
courseId?: number;
};
export type AddonModWorkshopAssessmentStrategyFieldErrors = Record<string, string>;

View File

@ -0,0 +1,27 @@
<core-loading [hideUntil]="loaded">
<ion-item class="ion-text-wrap" [detail]="canViewAssessment && !canSelfAssess" (click)="gotoAssessment($event)">
<core-user-avatar [user]="profile" slot="start" [courseId]="courseId" [userId]="profile?.id"></core-user-avatar>
<ion-label>
<h2 *ngIf="profile && profile.fullname">{{profile.fullname}}</h2>
<p *ngIf="showGrade(assessment.grade)">
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}}
</p>
<p *ngIf="access.canviewallsubmissions && !showGrade(assessment.gradinggradeover) && showGrade(assessment.gradinggrade)">
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}}
</p>
<p *ngIf="access.canviewallsubmissions && showGrade(assessment.gradinggradeover)" class="core-overriden-grade">
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggradeover}}
</p>
<p *ngIf="assessment.weight && assessment.weight != 1">
{{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }}
</p>
<ion-badge *ngIf="!assessment.grade" color="danger">{{ 'addon.mod_workshop.notassessed' | translate }}</ion-badge>
<ion-button expand="block" *ngIf="canSelfAssess && !showGrade(assessment.grade)" (click)="gotoOwnAssessment($event)">
{{ 'addon.mod_workshop.assess' | translate }}
</ion-button>
</ion-label>
<ion-note slot="end" *ngIf="offline">
<ion-icon name="fas-clock"></ion-icon>{{ 'core.notsent' | translate }}
</ion-note>
</ion-item>
</core-loading>

View File

@ -0,0 +1,170 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit } from '@angular/core';
import { Params } from '@angular/router';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { AddonModWorkshopData, AddonModWorkshopGetWorkshopAccessInformationWSResponse } from '../../services/workshop';
import {
AddonModWorkshopHelper,
AddonModWorkshopSubmissionAssessmentWithFormData,
AddonModWorkshopSubmissionDataWithOfflineData,
} from '../../services/workshop-helper';
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
/**
* Component that displays workshop assessment.
*/
@Component({
selector: 'addon-mod-workshop-assessment',
templateUrl: 'addon-mod-workshop-assessment.html',
})
export class AddonModWorkshopAssessmentComponent implements OnInit {
@Input() assessment!: AddonModWorkshopSubmissionAssessmentWithFormData;
@Input() courseId!: number;
@Input() workshop!: AddonModWorkshopData;
@Input() access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
@Input() protected submission!: AddonModWorkshopSubmissionDataWithOfflineData;
@Input() protected module!: CoreCourseModule;
canViewAssessment = false;
canSelfAssess = false;
profile?: CoreUserProfile;
showGrade: (grade?: string | number) => boolean;
offline = false;
loaded = false;
protected currentUserId: number;
protected assessmentId?: number;
constructor() {
this.currentUserId = CoreSites.getCurrentSiteUserId();
this.showGrade = AddonModWorkshopHelper.showGrade;
}
/**
* Component being initialized.
*/
ngOnInit(): void {
const canAssess = this.access && this.access.assessingallowed;
const userId = this.assessment.reviewerid;
const promises: Promise<void>[] = [];
this.assessmentId = this.assessment.id;
this.canViewAssessment = !!this.assessment.grade;
this.canSelfAssess = canAssess && userId == this.currentUserId;
if (userId) {
promises.push(CoreUser.getProfile(userId, this.courseId, true).then((profile) => {
this.profile = profile;
return;
}));
}
let assessOffline: Promise<void>;
if (userId == this.currentUserId) {
assessOffline = AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId) .then((offlineAssess) => {
this.offline = true;
this.assessment.weight = <number>offlineAssess.inputdata.weight;
return;
});
} else {
assessOffline = AddonModWorkshopOffline.getEvaluateAssessment(this.workshop.id, this.assessmentId)
.then((offlineAssess) => {
this.offline = true;
this.assessment.gradinggradeover = offlineAssess.gradinggradeover;
this.assessment.weight = <number>offlineAssess.weight;
return;
});
}
promises.push(assessOffline.catch(() => {
this.offline = false;
// Ignore errors.
}));
Promise.all(promises).finally(() => {
this.loaded = true;
});
}
/**
* Navigate to the assessment.
*/
async gotoAssessment(event: Event): Promise<void> {
if (!this.canSelfAssess && this.canViewAssessment) {
event.preventDefault();
event.stopPropagation();
const params: Params = {
assessment: this.assessment,
submission: this.submission,
profile: this.profile,
};
if (!this.submission) {
const modal = await CoreDomUtils.showModalLoading();
try {
params.submission = await AddonModWorkshopHelper.getSubmissionById(
this.workshop.id,
this.assessment.submissionid,
{ cmId: this.workshop.coursemodule },
);
CoreNavigator.navigate(String(this.assessmentId), { params });
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Cannot load submission');
} finally {
modal.dismiss();
}
} else {
CoreNavigator.navigate(String(this.assessmentId), { params });
}
}
}
/**
* Navigate to my own assessment.
*/
gotoOwnAssessment(event: Event): void {
if (!this.canSelfAssess) {
return;
}
event.preventDefault();
event.stopPropagation();
const params: Params = {
module: this.module,
workshop: this.workshop,
access: this.access,
profile: this.profile,
submission: this.submission,
assessment: this.assessment,
};
CoreNavigator.navigate(String(this.submission.id), params);
}
}

View File

@ -0,0 +1,107 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="workshop && workshop.coursemodule"
[courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end" [hidden]="!evaluating">
<ion-button fill="clear" (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate">
{{ 'core.save' | translate }}
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshAssessment($event.target)" *ngIf="!evaluating">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-item class="ion-text-wrap">
<core-user-avatar *ngIf="profile" [user]="profile" slot="start" [courseId]="courseId" [userId]="profile.id">
</core-user-avatar>
<ion-label>
<h2 *ngIf="profile && profile.fullname">{{profile.fullname}}</h2>
<p *ngIf="workshop && assessment && showGrade(assessment.grade)">
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}}
</p>
<p *ngIf="workshop && access && access.canviewallsubmissions && assessment && showGrade(assessment.gradinggrade)"
[class.core-has-overriden-grade]=" showGrade(assessment.gradinggrade)">
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}}
</p>
<p *ngIf="access && access.canviewallsubmissions && assessment && showGrade(assessment.gradinggradeover)"
class="core-overriden-grade">
{{ 'addon.mod_workshop.gradinggradeover' | translate }}: {{assessment.gradinggradeover}}
</p>
<p *ngIf="assessment && assessment.weight && assessment.weight != 1">
{{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }}
</p>
<ion-badge *ngIf="!assessment || !showGrade(assessment.grade)" color="danger">
{{ 'addon.mod_workshop.notassessed' | translate }}
</ion-badge>
</ion-label>
</ion-item>
<addon-mod-workshop-assessment-strategy
*ngIf="assessment && assessmentId && showGrade(assessment.grade) && workshop && access" [workshop]="workshop"
[access]="access" [assessmentId]="assessmentId" [userId]="profile && profile.id" [strategy]="strategy">
</addon-mod-workshop-assessment-strategy>
<form ion-list [formGroup]="evaluateForm" *ngIf="evaluating" #evaluateFormEl>
<ion-item class="ion-text-wrap">
<ion-label><h2>{{ 'addon.mod_workshop.assessmentsettings' | translate }}</h2></ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="access?.canallocate">
<ion-label position="stacked">
<span core-mark-required="true">
{{ 'addon.mod_workshop.assessmentweight' | translate }}
</span>
</ion-label>
<ion-select formControlName="weight" required="true" interface="action-sheet">
<ion-select-option *ngFor="let w of weights" [value]="w">{{ w }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.gradinggradecalculated' | translate }}</h2>
<p>{{ assessment.gradinggrade }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="access?.canoverridegrades">
<ion-label position="stacked">{{ 'addon.mod_workshop.gradinggradeover' | translate }}</ion-label>
<ion-select formControlName="grade" interface="action-sheet">
<ion-select-option *ngFor="let grade of evaluationGrades" [value]="grade.value">
{{grade.label}}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item *ngIf="access?.canoverridegrades">
<ion-label position="stacked">{{ 'addon.mod_workshop.feedbackreviewer' | translate }}</ion-label>
<core-rich-text-editor [control]="evaluateForm.controls['text']" name="text"
[autoSave]="true" contextLevel="module" [contextInstanceId]="workshop?.coursemodule"
elementId="feedbackreviewer_editor" [draftExtraParams]="{asid: assessmentId}">
</core-rich-text-editor>
</ion-item>
</form>
<ion-list *ngIf="!evaluating && evaluate && evaluate.text">
<ion-item class="ion-text-wrap">
<core-user-avatar *ngIf="evaluateByProfile" [user]="evaluateByProfile" slot="start"
[courseId]="courseId" [userId]="evaluateByProfile.id"></core-user-avatar>
<ion-label>
<h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname">
{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}
</h2>
<core-format-text [text]="evaluate.text" contextLevel="module" [contextInstanceId]="workshop?.coursemodule"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,398 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { CoreCourse } from '@features/course/services/course';
import { CoreGradesHelper, CoreGradesMenuItem } from '@features/grades/services/grades-helper';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CanLeave } from '@guards/can-leave';
import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreForms } from '@singletons/form';
import {
AddonModWorkshop,
AddonModWorkshopAssessmentSavedChangedEventData,
AddonModWorkshopData,
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
AddonModWorkshopPhase,
AddonModWorkshopProvider,
AddonModWorkshopSubmissionData,
} from '../../services/workshop';
import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper';
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
import { AddonModWorkshopSyncProvider } from '../../services/workshop-sync';
/**
* Page that displays a workshop assessment.
*/
@Component({
selector: 'page-addon-mod-workshop-assessment-page',
templateUrl: 'assessment.html',
})
export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLeave {
@ViewChild('evaluateFormEl') formElement!: ElementRef;
assessment!: AddonModWorkshopSubmissionAssessmentWithFormData;
submission!: AddonModWorkshopSubmissionData;
profile!: CoreUserProfile;
courseId!: number;
access?: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
assessmentId!: number;
evaluating = false;
loaded = false;
showGrade: (grade?: string | number) => boolean;
evaluateForm: FormGroup;
maxGrade?: number;
workshop?: AddonModWorkshopData;
strategy?: string;
title = '';
evaluate: AddonModWorkshopAssessmentEvaluation = {
text: '',
grade: -1,
weight: 1,
};
weights: number[] = [];
evaluateByProfile?: CoreUserProfile;
evaluationGrades: CoreGradesMenuItem[] =[];
protected workshopId!: number;
protected originalEvaluation: AddonModWorkshopAssessmentEvaluation = {
text: '',
grade: -1,
weight: 1,
};
protected hasOffline = false;
protected syncObserver: CoreEventObserver;
protected isDestroyed = false;
protected siteId: string;
protected currentUserId: number;
protected forceLeave = false;
constructor(
protected fb: FormBuilder,
) {
this.siteId = CoreSites.getCurrentSiteId();
this.currentUserId = CoreSites.getCurrentSiteUserId();
this.showGrade = AddonModWorkshopHelper.showGrade;
this.evaluateForm = new FormGroup({});
this.evaluateForm.addControl('weight', this.fb.control('', Validators.required));
this.evaluateForm.addControl('grade', this.fb.control(''));
this.evaluateForm.addControl('text', this.fb.control(''));
// Refresh workshop on sync.
this.syncObserver = CoreEvents.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => {
// Update just when all database is synced.
if (this.workshopId === data.workshopId) {
this.loaded = false;
this.refreshAllData();
}
}, this.siteId);
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.assessment = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionAssessmentWithFormData>('assessment')!;
this.submission = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionData>('submission')!;
this.profile = CoreNavigator.getRouteParam<CoreUserProfile>('profile')!;
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.assessmentId = this.assessment.id;
this.workshopId = this.submission.workshopid;
this.fetchAssessmentData();
}
/**
* Check if we can leave the page or not.
*
* @return Resolved if we can leave it, rejected if not.
*/
async canLeave(): Promise<boolean> {
if (this.forceLeave || !this.evaluating) {
return true;
}
if (!this.hasEvaluationChanged()) {
return true;
}
// Show confirmation if some data has been modified.
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
return true;
}
/**
* Fetch the assessment data.
*
* @return Resolved when done.
*/
protected async fetchAssessmentData(): Promise<void> {
try {
this.workshop = await AddonModWorkshop.getWorkshopById(this.courseId, this.workshopId);
this.title = this.workshop.name;
this.strategy = this.workshop.strategy;
const gradeInfo = await CoreCourse.getModuleBasicGradeInfo(this.workshop.coursemodule);
this.maxGrade = gradeInfo?.grade;
this.access = await AddonModWorkshop.getWorkshopAccessInformation(
this.workshopId,
{ cmId: this.workshop.coursemodule },
);
// Load Weights selector.
if (this.assessmentId && (this.access.canallocate || this.access.canoverridegrades)) {
if (!this.isDestroyed) {
// Block the workshop.
CoreSync.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId);
}
this.evaluating = true;
} else {
this.evaluating = false;
}
if (!this.evaluating && this.workshop.phase != AddonModWorkshopPhase.PHASE_CLOSED) {
return;
}
// Get all info of the assessment.
const assessment = await AddonModWorkshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, {
userId: this.profile && this.profile.id,
cmId: this.workshop.coursemodule,
});
this.assessment = AddonModWorkshopHelper.realGradeValue(this.workshop, assessment);
this.evaluate.text = this.assessment.feedbackreviewer || '';
this.evaluate.weight = this.assessment.weight;
if (this.evaluating) {
if (this.access.canallocate) {
this.weights = [];
for (let i = 16; i >= 0; i--) {
this.weights[i] = i;
}
}
if (this.access.canoverridegrades) {
const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
this.evaluationGrades =
await CoreGradesHelper.makeGradesMenu(this.workshop.gradinggrade, undefined, defaultGrade, -1);
}
try {
const offlineAssess = await AddonModWorkshopOffline.getEvaluateAssessment(this.workshopId, this.assessmentId);
this.hasOffline = true;
this.evaluate.weight = offlineAssess.weight;
if (this.access.canoverridegrades) {
this.evaluate.text = offlineAssess.feedbacktext || '';
this.evaluate.grade = parseInt(offlineAssess.gradinggradeover, 10) || -1;
}
} catch {
this.hasOffline = false;
// No offline, load online.
if (this.access.canoverridegrades) {
this.evaluate.text = this.assessment.feedbackreviewer || '';
this.evaluate.grade = parseInt(String(this.assessment.gradinggradeover), 10) || -1;
}
} finally {
this.originalEvaluation.weight = this.evaluate.weight;
if (this.access.canoverridegrades) {
this.originalEvaluation.text = this.evaluate.text;
this.originalEvaluation.grade = this.evaluate.grade;
}
this.evaluateForm.controls['weight'].setValue(this.evaluate.weight);
if (this.access.canoverridegrades) {
this.evaluateForm.controls['grade'].setValue(this.evaluate.grade);
this.evaluateForm.controls['text'].setValue(this.evaluate.text);
}
}
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.assessment.gradinggradeoverby) {
this.evaluateByProfile = await CoreUser.getProfile(this.assessment.gradinggradeoverby, this.courseId, true);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'mm.course.errorgetmodule', true);
} finally {
this.loaded = true;
}
}
/**
* Force leaving the page, without checking for changes.
*/
protected forceLeavePage(): void {
this.forceLeave = true;
CoreNavigator.back();
}
/**
* Check if data has changed.
*
* @return True if changed, false otherwise.
*/
protected hasEvaluationChanged(): boolean {
if (!this.loaded || !this.evaluating) {
return false;
}
const inputData = this.evaluateForm.value;
if (this.originalEvaluation.weight != inputData.weight) {
return true;
}
if (this.access && this.access.canoverridegrades) {
if (this.originalEvaluation.text != inputData.text) {
return true;
}
if (this.originalEvaluation.grade != inputData.grade) {
return true;
}
}
return false;
}
/**
* Convenience function to refresh all the data.
*
* @return Resolved when done.
*/
protected async refreshAllData(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModWorkshop.invalidateWorkshopData(this.courseId));
promises.push(AddonModWorkshop.invalidateWorkshopAccessInformationData(this.workshopId));
promises.push(AddonModWorkshop.invalidateReviewerAssesmentsData(this.workshopId));
if (this.assessmentId) {
promises.push(AddonModWorkshop.invalidateAssessmentFormData(this.workshopId, this.assessmentId));
promises.push(AddonModWorkshop.invalidateAssessmentData(this.workshopId, this.assessmentId));
}
try {
await Promise.all(promises);
} finally {
CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, null, this.siteId);
await this.fetchAssessmentData();
}
}
/**
* Pull to refresh.
*
* @param refresher Refresher.
*/
refreshAssessment(refresher: IonRefresher): void {
if (this.loaded) {
this.refreshAllData().finally(() => {
refresher?.complete();
});
}
}
/**
* Save the assessment evaluation.
*/
async saveEvaluation(): Promise<void> {
// Check if data has changed.
if (this.hasEvaluationChanged()) {
await this.sendEvaluation();
}
// Go back.
this.forceLeavePage();
}
/**
* Sends the evaluation to be saved on the server.
*
* @return Resolved when done.
*/
protected async sendEvaluation(): Promise<void> {
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
const inputData: AddonModWorkshopAssessmentEvaluation = this.evaluateForm.value;
const grade = inputData.grade >= 0 ? String(inputData.grade) : '';
// Add some HTML to the message if needed.
const text = CoreTextUtils.formatHtmlLines(inputData.text);
try {
// Try to send it to server.
const result = await AddonModWorkshop.evaluateAssessment(
this.workshopId,
this.assessmentId,
this.courseId,
text,
inputData.weight,
grade,
);
CoreForms.triggerFormSubmittedEvent(this.formElement, !!result, this.siteId);
const data: AddonModWorkshopAssessmentSavedChangedEventData = {
workshopId: this.workshopId,
assessmentId: this.assessmentId,
userId: this.currentUserId,
};
return AddonModWorkshop.invalidateAssessmentData(this.workshopId, this.assessmentId).finally(() => {
CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, data, this.siteId);
});
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Cannot save assessment evaluation');
} finally {
modal.dismiss();
}
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
this.syncObserver?.off();
// Restore original back functions.
CoreSync.unblockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId);
}
}
type AddonModWorkshopAssessmentEvaluation = {
text: string;
grade: number;
weight: number;
};

View File

@ -0,0 +1,46 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.mod_workshop.editsubmission' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="save()" [attr.aria-label]="'core.save' | translate">
{{ 'core.save' | translate }}
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="editForm" *ngIf="workshop" #editFormEl>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<span core-mark-required="true">
{{ 'addon.mod_workshop.submissiontitle' | translate }}
</span>
</ion-label>
<ion-input name="title" type="text" [placeholder]="'addon.mod_workshop.submissiontitle' | translate"
formControlName="title">
</ion-input>
</ion-item>
<ion-item *ngIf="textAvailable">
<ion-label position="stacked">
<span [core-mark-required]="textRequired">
{{ 'addon.mod_workshop.submissioncontent' | translate }}
</span>
</ion-label>
<core-rich-text-editor [control]="editForm.controls['content']" name="content"
[placeholder]="'addon.mod_workshop.submissioncontent' | translate" name="content" [component]="component"
[componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id"
elementId="content_editor" [draftExtraParams]="editorExtraParams"></core-rich-text-editor>
</ion-item>
<core-attachments *ngIf="fileAvailable" [files]="submission?.attachmentfiles || []" [maxSize]="workshop.maxbytes"
[maxSubmissions]="workshop.nattachments" [component]="component" [componentId]="workshop.coursemodule"
allowOffline="true" [acceptedTypes]="workshop.submissionfiletypes" [required]="fileRequired">
</core-attachments>
</form>
</core-loading>
</ion-content>

View File

@ -0,0 +1,476 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { CoreError } from '@classes/errors/error';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CanLeave } from '@guards/can-leave';
import { CoreFile } from '@services/file';
import { CoreFileEntry, CoreFileHelper } from '@services/file-helper';
import { CoreFileSession } from '@services/file-session';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { CoreForms } from '@singletons/form';
import {
AddonModWorkshopProvider,
AddonModWorkshop,
AddonModWorkshopSubmissionType,
AddonModWorkshopSubmissionChangedEventData,
AddonModWorkshopAction,
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
AddonModWorkshopData,
} from '../../services/workshop';
import { AddonModWorkshopHelper, AddonModWorkshopSubmissionDataWithOfflineData } from '../../services/workshop-helper';
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
/**
* Page that displays the workshop edit submission.
*/
@Component({
selector: 'page-addon-mod-workshop-edit-submission',
templateUrl: 'edit-submission.html',
})
export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, CanLeave {
@ViewChild('editFormEl') formElement!: ElementRef;
module!: CoreCourseModule;
courseId!: number;
access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
submission?: AddonModWorkshopSubmissionDataWithOfflineData;
loaded = false;
component = AddonModWorkshopProvider.COMPONENT;
componentId!: number;
editForm: FormGroup; // The form group.
editorExtraParams: Record<string, unknown> = {}; // Extra params to identify the draft.
workshop?: AddonModWorkshopData;
textAvailable = false;
textRequired = false;
fileAvailable = false;
fileRequired = false;
protected workshopId!: number;
protected submissionId = 0;
protected userId: number;
protected originalData: AddonModWorkshopEditSubmissionInputData = {
title: '',
content: '',
attachmentfiles: [],
};
protected hasOffline = false;
protected editing = false;
protected forceLeave = false;
protected siteId: string;
protected isDestroyed = false;
constructor(
protected fb: FormBuilder,
) {
this.userId = CoreSites.getCurrentSiteUserId();
this.siteId = CoreSites.getCurrentSiteId();
this.editForm = new FormGroup({});
this.editForm.addControl('title', this.fb.control('', Validators.required));
this.editForm.addControl('content', this.fb.control(''));
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!;
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.access = CoreNavigator.getRouteParam<AddonModWorkshopGetWorkshopAccessInformationWSResponse>('access')!;
this.submissionId = CoreNavigator.getRouteNumberParam('submissionId') || 0;
if (this.submissionId > 0) {
this.editorExtraParams.id = this.submissionId;
}
this.workshopId = this.module.instance!;
this.componentId = this.module.id;
if (!this.isDestroyed) {
// Block the workshop.
CoreSync.blockOperation(this.component, this.workshopId);
}
this.fetchSubmissionData();
}
/**
* Check if we can leave the page or not.
*
* @return Resolved if we can leave it, rejected if not.
*/
async canLeave(): Promise<boolean> {
if (this.forceLeave) {
return true;
}
// Check if data has changed.
if (this.hasDataChanged()) {
// Show confirmation if some data has been modified.
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
}
if (this.submission?.attachmentfiles) {
// Delete the local files from the tmp folder.
CoreFileUploader.clearTmpFiles(this.submission.attachmentfiles);
}
CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
return true;
}
/**
* Fetch the submission data.
*
* @return Resolved when done.
*/
protected async fetchSubmissionData(): Promise<void> {
try {
this.workshop = await AddonModWorkshop.getWorkshop(this.courseId, this.module.id);
this.textAvailable = (this.workshop.submissiontypetext != AddonModWorkshopSubmissionType.SUBMISSION_TYPE_DISABLED);
this.textRequired = (this.workshop.submissiontypetext == AddonModWorkshopSubmissionType.SUBMISSION_TYPE_REQUIRED);
this.fileAvailable = (this.workshop.submissiontypefile != AddonModWorkshopSubmissionType.SUBMISSION_TYPE_DISABLED);
this.fileRequired = (this.workshop.submissiontypefile == AddonModWorkshopSubmissionType.SUBMISSION_TYPE_REQUIRED);
this.editForm.controls.content.setValidators(this.textRequired ? Validators.required : null);
if (this.submissionId > 0) {
this.editing = true;
this.submission =
await AddonModWorkshopHelper.getSubmissionById(this.workshopId, this.submissionId, { cmId: this.module.id });
const canEdit = this.userId == this.submission.authorid &&
this.access.cansubmit &&
this.access.modifyingsubmissionallowed;
if (!canEdit) {
// Should not happen, but go back if does.
this.forceLeavePage();
return;
}
} else if (!this.access.cansubmit || !this.access.creatingsubmissionallowed) {
// Should not happen, but go back if does.
this.forceLeavePage();
return;
}
const submissionsActions = await AddonModWorkshopOffline.getSubmissions(this.workshopId);
if (submissionsActions && submissionsActions.length) {
this.hasOffline = true;
this.submission = await AddonModWorkshopHelper.applyOfflineData(this.submission, submissionsActions);
} else {
this.hasOffline = false;
}
if (this.submission) {
this.originalData.title = this.submission.title || '';
this.originalData.content = this.submission.content || '';
this.originalData.attachmentfiles = [];
(this.submission.attachmentfiles || []).forEach((file) => {
let filename = CoreFile.getFileName(file);
if (!filename) {
// We don't have filename, extract it from the path.
filename = CoreFileHelper.getFilenameFromPath(file) || '';
}
this.originalData.attachmentfiles.push({
filename,
fileurl: 'fileurl' in file ? file.fileurl : '',
});
});
this.editForm.controls['title'].setValue(this.submission.title);
this.editForm.controls['content'].setValue(this.submission.content);
}
CoreFileSession.setFiles(
this.component,
this.getFilesComponentId(),
this.submission?.attachmentfiles || [],
);
this.loaded = true;
} catch (error) {
this.loaded = false;
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
this.forceLeavePage();
}
}
/**
* Force leaving the page, without checking for changes.
*/
protected forceLeavePage(): void {
this.forceLeave = true;
CoreNavigator.back();
}
/**
* Get the form input data.
*
* @return Object with all the info.
*/
protected getInputData(): AddonModWorkshopEditSubmissionInputData {
const values: AddonModWorkshopEditSubmissionInputData = {
title: this.editForm.value.title,
content: '',
attachmentfiles: [],
};
if (this.textAvailable) {
values.content = this.editForm.value.content || '';
}
if (this.fileAvailable) {
values.attachmentfiles = CoreFileSession.getFiles(this.component, this.getFilesComponentId()) || [];
}
return values;
}
/**
* Check if data has changed.
*
* @return True if changed or false if not.
*/
protected hasDataChanged(): boolean {
if (!this.loaded) {
return false;
}
const inputData = this.getInputData();
if (this.originalData.title != inputData.title || this.textAvailable && this.originalData.content != inputData.content) {
return true;
}
if (this.fileAvailable) {
return CoreFileUploader.areFileListDifferent(inputData.attachmentfiles, this.originalData.attachmentfiles);
}
return false;
}
/**
* Save the submission.
*/
async save(): Promise<void> {
// Check if data has changed.
if (this.hasDataChanged()) {
try {
await this.saveSubmission();
// Go back to entry list.
this.forceLeavePage();
} catch{
// Nothing to do.
}
} else {
// Nothing to save, just go back.
this.forceLeavePage();
}
}
/**
* Send submission and save.
*
* @return Resolved when done.
*/
protected async saveSubmission(): Promise<void> {
const inputData = this.getInputData();
if (!inputData.title) {
CoreDomUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredtitle');
throw new CoreError(Translate.instant('addon.mod_workshop.submissionrequiredtitle'));
}
const noText = CoreTextUtils.htmlIsBlank(inputData.content);
const noFiles = !inputData.attachmentfiles.length;
if ((this.textRequired && noText) || (this.fileRequired && noFiles) || (noText && noFiles)) {
CoreDomUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredcontent');
throw new CoreError(Translate.instant('addon.mod_workshop.submissionrequiredcontent'));
}
let saveOffline = false;
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
const submissionId = this.submission?.id;
// Add some HTML to the message if needed.
if (this.textAvailable) {
inputData.content = CoreTextUtils.formatHtmlLines(inputData.content);
}
// Upload attachments first if any.
let allowOffline = !inputData.attachmentfiles.length;
try {
let attachmentsId: CoreFileUploaderStoreFilesResult | number | undefined;
try {
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreSubmissionFiles(
this.workshopId,
inputData.attachmentfiles,
false,
);
} catch {
// Cannot upload them in online, save them in offline.
saveOffline = true;
allowOffline = true;
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreSubmissionFiles(
this.workshopId,
inputData.attachmentfiles,
true,
);
}
if (!saveOffline && !this.fileAvailable) {
attachmentsId = undefined;
}
let newSubmissionId: number | false;
if (this.editing) {
if (saveOffline) {
// Save submission in offline.
await AddonModWorkshopOffline.saveSubmission(
this.workshopId,
this.courseId,
inputData.title,
inputData.content,
attachmentsId as CoreFileUploaderStoreFilesResult,
submissionId,
AddonModWorkshopAction.UPDATE,
);
newSubmissionId = false;
} else {
// Try to send it to server.
// Don't allow offline if there are attachments since they were uploaded fine.
newSubmissionId = await AddonModWorkshop.updateSubmission(
this.workshopId,
submissionId!,
this.courseId,
inputData.title,
inputData.content,
attachmentsId,
undefined,
allowOffline,
);
}
} else {
if (saveOffline) {
// Save submission in offline.
await AddonModWorkshopOffline.saveSubmission(
this.workshopId,
this.courseId,
inputData.title,
inputData.content,
attachmentsId as CoreFileUploaderStoreFilesResult,
undefined,
AddonModWorkshopAction.ADD,
);
newSubmissionId = false;
} else {
// Try to send it to server.
// Don't allow offline if there are attachments since they were uploaded fine.
newSubmissionId = await AddonModWorkshop.addSubmission(
this.workshopId,
this.courseId,
inputData.title,
inputData.content,
attachmentsId,
undefined,
allowOffline,
);
}
}
CoreForms.triggerFormSubmittedEvent(this.formElement, !!newSubmissionId, this.siteId);
const data: AddonModWorkshopSubmissionChangedEventData = {
workshopId: this.workshopId,
};
if (newSubmissionId) {
// Data sent to server, delete stored files (if any).
AddonModWorkshopOffline.deleteSubmissionAction(
this.workshopId,
this.editing ? AddonModWorkshopAction.UPDATE : AddonModWorkshopAction.ADD,
);
AddonModWorkshopHelper.deleteSubmissionStoredFiles(this.workshopId, this.siteId);
data.submissionId = newSubmissionId;
}
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'workshop' });
const promise = newSubmissionId ? AddonModWorkshop.invalidateSubmissionData(this.workshopId, newSubmissionId) :
Promise.resolve();
await promise.finally(() => {
CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
// Delete the local files from the tmp folder.
CoreFileUploader.clearTmpFiles(inputData.attachmentfiles);
});
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Cannot save submission');
} finally {
modal.dismiss();
}
}
protected getFilesComponentId(): string {
const id = this.submissionId > 0
? this.submissionId
: 'newsub';
return this.workshopId + '_' + id;
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
CoreSync.unblockOperation(this.component, this.workshopId);
}
}
type AddonModWorkshopEditSubmissionInputData = {
title: string;
content: string;
attachmentfiles: CoreFileEntry[];
};

View File

@ -0,0 +1,154 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text *ngIf="title" [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end" [hidden]="!loaded">
<ion-button *ngIf="assessmentId && access.assessingallowed" fill="clear" (click)="saveAssessment()"
[attr.aria-label]="'core.save' | translate">
{{ 'core.save' | translate }}
</ion-button>
<ion-button *ngIf="canAddFeedback" fill="clear" (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate">
{{ 'core.save' | translate }}
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)"
*ngIf="!((assessmentId && access.assessingallowed) || canAddFeedback)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-list *ngIf="submission">
<addon-mod-workshop-submission [submission]="submission" [courseId]="courseId" [module]="module" [workshop]="workshop"
[access]="access">
</addon-mod-workshop-submission>
<ion-item class="ion-text-wrap" *ngIf="canEdit || canDelete">
<ion-label>
<ion-button expand="block" *ngIf="canEdit" (click)="editSubmission()">
<ion-icon name="fas-edit" slot="start"></ion-icon>
{{ 'addon.mod_workshop.editsubmission' | translate }}
</ion-button>
<ion-button expand="block" *ngIf="!submission.deleted && canDelete" color="danger" (click)="deleteSubmission()">
<ion-icon name="fas-trash" slot="start"></ion-icon>
{{ 'addon.mod_workshop.deletesubmission' | translate }}
</ion-button>
<ion-button expand="block" fill="outline" *ngIf="submission.deleted && canDelete" color="danger"
(click)="undoDeleteSubmission()">
<ion-icon name="fas-undo-alt" slot="start"></ion-icon>
{{ 'core.restore' | translate }}
</ion-button>
</ion-label>
</ion-item>
</ion-list>
<ion-list *ngIf="!canAddFeedback && evaluate?.text">
<ion-item class="ion-text-wrap">
<core-user-avatar *ngIf="evaluateByProfile" [user]="evaluateByProfile" slot="start" [courseId]="courseId"
[userId]="evaluateByProfile.id"></core-user-avatar>
<ion-label>
<h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname">
{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}
</h2>
<core-format-text [text]="evaluate?.text" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-list>
<ion-list *ngIf="ownAssessment && !assessment">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.yourassessment' | translate }}</h2>
</ion-label>
</ion-item>
<addon-mod-workshop-assessment [submission]="submission" [assessment]="ownAssessment" [courseId]="courseId"
[access]="access" [module]="module" [workshop]="workshop">
</addon-mod-workshop-assessment>
</ion-list>
<ion-list *ngIf="submissionInfo && submissionInfo.reviewedby && submissionInfo.reviewedby.length && !assessment">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.receivedgrades' | translate }}</h2>
</ion-label>
</ion-item>
<ng-container *ngFor="let reviewer of submissionInfo.reviewedby">
<addon-mod-workshop-assessment *ngIf="!reviewer.ownAssessment" [submission]="submission" [assessment]="reviewer"
[courseId]="courseId" [access]="access" [module]="module" [workshop]="workshop">
</addon-mod-workshop-assessment>
</ng-container>
</ion-list>
<ion-list *ngIf="submissionInfo && submissionInfo.reviewerof && submissionInfo.reviewerof.length && !assessment">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.givengrades' | translate }}</h2>
</ion-label>
</ion-item>
<addon-mod-workshop-assessment *ngFor="let reviewer of submissionInfo.reviewerof" [assessment]="reviewer"
[courseId]="courseId" [module]="module" [workshop]="workshop" [access]="access">
</addon-mod-workshop-assessment>
</ion-list>
<form ion-list [formGroup]="feedbackForm" *ngIf="canAddFeedback && submission" #feedbackFormEl>
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="access.canpublishsubmissions">
<ion-label>{{ 'addon.mod_workshop.publishsubmission' | translate }}</ion-label>
<ion-toggle formControlName="published"></ion-toggle>
<p class="item-help">{{ 'addon.mod_workshop.publishsubmission_help' | translate }}</p>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.gradecalculated' | translate }}</h2>
<p>{{ submission.grade }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">{{ 'addon.mod_workshop.gradeover' | translate }}</ion-label>
<ion-select formControlName="grade" interface="action-sheet">
<ion-select-option *ngFor="let grade of evaluationGrades" [value]="grade.value">
{{grade.label}}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label>
<core-rich-text-editor [control]="feedbackForm.controls['text']" name="text"
[autoSave]="true" contextLevel="module" [contextInstanceId]="module.id" elementId="feedbackauthor_editor"
[draftExtraParams]="{id: submissionId}">
</core-rich-text-editor>
</ion-item>
</form>
<addon-mod-workshop-assessment-strategy *ngIf="assessmentId" [workshop]="workshop" [access]="access"
[assessmentId]="assessmentId" [userId]="assessmentUserId" [strategy]="strategy" [edit]="access.assessingallowed">
</addon-mod-workshop-assessment-strategy>
<ion-list *ngIf="assessmentId && !access.assessingallowed && assessment?.feedbackreviewer">
<ion-item class="ion-text-wrap">
<core-user-avatar *ngIf="evaluateGradingByProfile" [user]="evaluateGradingByProfile" slot="start"
[courseId]="courseId" [userId]="evaluateGradingByProfile.id"></core-user-avatar>
<ion-label>
<h2 *ngIf="evaluateGradingByProfile && evaluateGradingByProfile.fullname">
{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateGradingByProfile.fullname} }}
</h2>
<core-format-text [text]="assessment!.feedbackreviewer" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,610 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, Optional, ViewChild, ElementRef } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Params } from '@angular/router';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreGradesHelper, CoreGradesMenuItem } from '@features/grades/services/grades-helper';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CanLeave } from '@guards/can-leave';
import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreForms } from '@singletons/form';
import { AddonModWorkshopAssessmentStrategyComponent } from '../../components/assessment-strategy/assessment-strategy';
import {
AddonModWorkshopProvider,
AddonModWorkshop,
AddonModWorkshopPhase,
AddonModWorkshopSubmissionChangedEventData,
AddonModWorkshopAction,
AddonModWorkshopData,
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
AddonModWorkshopAssessmentSavedChangedEventData,
} from '../../services/workshop';
import {
AddonModWorkshopHelper,
AddonModWorkshopSubmissionAssessmentWithFormData,
AddonModWorkshopSubmissionDataWithOfflineData,
} from '../../services/workshop-helper';
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
import { AddonModWorkshopSyncProvider, AddonModWorkshopAutoSyncData } from '../../services/workshop-sync';
/**
* Page that displays a workshop submission.
*/
@Component({
selector: 'page-addon-mod-workshop-submission-page',
templateUrl: 'submission.html',
})
export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLeave {
@ViewChild(AddonModWorkshopAssessmentStrategyComponent) assessmentStrategy?: AddonModWorkshopAssessmentStrategyComponent;
@ViewChild('feedbackFormEl') formElement?: ElementRef;
module!: CoreCourseModule;
workshop!: AddonModWorkshopData;
access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
assessment?: AddonModWorkshopSubmissionAssessmentWithFormData;
submissionInfo!: AddonModWorkshopSubmissionDataWithOfflineData;
profile?: CoreUserProfile;
courseId!: number;
submission?: AddonModWorkshopSubmissionDataWithOfflineData;
title?: string;
loaded = false;
ownAssessment?: AddonModWorkshopSubmissionAssessmentWithFormData;
strategy?: string;
assessmentId?: number;
assessmentUserId?: number;
evaluate?: AddonWorkshopSubmissionEvaluateData;
canAddFeedback = false;
canEdit = false;
canDelete = false;
evaluationGrades: CoreGradesMenuItem[] = [];
evaluateGradingByProfile?: CoreUserProfile;
evaluateByProfile?: CoreUserProfile;
feedbackForm: FormGroup; // The form group.
submissionId!: number;
protected workshopId!: number;
protected currentUserId: number;
protected userId?: number;
protected siteId: string;
protected originalEvaluation: Omit<AddonWorkshopSubmissionEvaluateData, 'grade'> & { grade: number | string} = {
published: false,
text: '',
grade: '',
};
protected hasOffline = false;
protected component = AddonModWorkshopProvider.COMPONENT;
protected forceLeave = false;
protected obsAssessmentSaved: CoreEventObserver;
protected syncObserver: CoreEventObserver;
protected isDestroyed = false;
constructor(
protected fb: FormBuilder,
@Optional() protected content: IonContent,
) {
this.currentUserId = CoreSites.getCurrentSiteUserId();
this.siteId = CoreSites.getCurrentSiteId();
this.feedbackForm = new FormGroup({});
this.feedbackForm.addControl('published', this.fb.control(''));
this.feedbackForm.addControl('grade', this.fb.control(''));
this.feedbackForm.addControl('text', this.fb.control(''));
this.obsAssessmentSaved = CoreEvents.on(AddonModWorkshopProvider.ASSESSMENT_SAVED, (data) => {
this.eventReceived(data);
}, this.siteId);
// Refresh workshop on sync.
this.syncObserver = CoreEvents.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => {
// Update just when all database is synced.
this.eventReceived(data);
}, this.siteId);
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
this.submissionId = CoreNavigator.getRouteNumberParam('submissionId')!;
this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!;
this.workshop = CoreNavigator.getRouteParam<AddonModWorkshopData>('workshop')!;
this.access = CoreNavigator.getRouteParam<AddonModWorkshopGetWorkshopAccessInformationWSResponse>('access')!;
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.profile = CoreNavigator.getRouteParam<CoreUserProfile>('profile');
this.submissionInfo = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionDataWithOfflineData>('submission')!;
this.assessment = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionAssessmentWithFormData>('assessment');
this.title = this.module.name;
this.workshopId = this.module.instance || this.workshop.id;
this.userId = this.submissionInfo?.authorid;
this.strategy = (this.assessment && this.assessment.strategy) || (this.workshop && this.workshop.strategy);
this.assessmentId = this.assessment?.id;
this.assessmentUserId = this.assessment?.reviewerid;
await this.fetchSubmissionData();
try {
await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId, this.workshop.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
* Check if we can leave the page or not.
*
* @return Resolved if we can leave it, rejected if not.
*/
async canLeave(): Promise<boolean> {
const assessmentHasChanged = this.assessmentStrategy?.hasDataChanged();
if (this.forceLeave || (!this.hasEvaluationChanged() && !assessmentHasChanged)) {
return true;
}
// Show confirmation if some data has been modified.
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
return true;
}
/**
* Goto edit submission page.
*/
editSubmission(): void {
const params: Params = {
module: module,
access: this.access,
};
CoreNavigator.navigate(String(this.submissionId) + '/edit', params);
}
/**
* Function called when we receive an event of submission changes.
*
* @param data Event data received.
*/
protected eventReceived(data: AddonModWorkshopAutoSyncData |
AddonModWorkshopAssessmentSavedChangedEventData): void {
if (this.workshopId === data.workshopId) {
this.content?.scrollToTop();
this.loaded = false;
this.refreshAllData();
}
}
/**
* Fetch the submission data.
*
* @return Resolved when done.
*/
protected async fetchSubmissionData(): Promise<void> {
try {
this.submission = await AddonModWorkshopHelper.getSubmissionById(this.workshopId, this.submissionId, {
cmId: this.module.id,
});
const promises: Promise<void>[] = [];
this.submission.grade = this.submissionInfo?.grade;
this.submission.gradinggrade = this.submissionInfo?.gradinggrade;
this.submission.gradeover = this.submissionInfo?.gradeover;
this.userId = this.submission.authorid || this.userId;
this.canEdit = this.currentUserId == this.userId && this.access.cansubmit && this.access.modifyingsubmissionallowed;
this.canDelete = this.access.candeletesubmissions;
this.canAddFeedback = !this.assessmentId && this.workshop.phase > AddonModWorkshopPhase.PHASE_ASSESSMENT &&
this.workshop.phase < AddonModWorkshopPhase.PHASE_CLOSED && this.access.canoverridegrades;
this.ownAssessment = undefined;
if (this.access.canviewallassessments) {
// Get new data, different that came from stateParams.
promises.push(AddonModWorkshop.getSubmissionAssessments(this.workshopId, this.submissionId, {
cmId: this.module.id,
}).then((subAssessments) => {
// Only allow the student to delete their own submission if it's still editable and hasn't been assessed.
if (this.canDelete) {
this.canDelete = !subAssessments.length;
}
this.submissionInfo.reviewedby = subAssessments;
this.submissionInfo.reviewedby.forEach((assessment) => {
assessment = AddonModWorkshopHelper.realGradeValue(this.workshop, assessment);
if (this.currentUserId == assessment.reviewerid) {
this.ownAssessment = assessment;
assessment.ownAssessment = true;
}
});
return;
}));
} else if (this.currentUserId == this.userId && this.assessmentId) {
// Get new data, different that came from stateParams.
promises.push(AddonModWorkshop.getAssessment(this.workshopId, this.assessmentId, {
cmId: this.module.id,
}).then((assessment) => {
// Only allow the student to delete their own submission if it's still editable and hasn't been assessed.
if (this.canDelete) {
this.canDelete = !assessment;
}
this.submissionInfo.reviewedby = [this.parseAssessment(assessment)];
return;
}));
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.userId == this.currentUserId) {
const assessments = await AddonModWorkshop.getSubmissionAssessments(this.workshopId, this.submissionId, {
cmId: this.module.id,
});
this.submissionInfo.reviewedby = assessments.map((assessment) => this.parseAssessment(assessment));
}
if (this.canAddFeedback || this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
this.evaluate = {
published: this.submission.published,
text: this.submission.feedbackauthor || '',
};
}
if (this.canAddFeedback) {
if (!this.isDestroyed) {
// Block the workshop.
CoreSync.blockOperation(this.component, this.workshopId);
}
const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
promises.push(CoreGradesHelper.makeGradesMenu(this.workshop.grade || 0, undefined, defaultGrade, -1)
.then(async (grades) => {
this.evaluationGrades = grades;
this.evaluate!.grade = {
label: CoreGradesHelper.getGradeLabelFromValue(grades, this.submissionInfo.gradeover) ||
defaultGrade,
value: this.submissionInfo.gradeover || -1,
};
try {
const offlineSubmission =
await AddonModWorkshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId);
this.hasOffline = true;
this.evaluate!.published = offlineSubmission.published;
this.evaluate!.text = offlineSubmission.feedbacktext;
this.evaluate!.grade = {
label: CoreGradesHelper.getGradeLabelFromValue(
grades,
parseInt(offlineSubmission.gradeover, 10),
) || defaultGrade,
value: offlineSubmission.gradeover || -1,
};
} catch {
// Ignore errors.
this.hasOffline = false;
} finally {
this.originalEvaluation.published = this.evaluate!.published;
this.originalEvaluation.text = this.evaluate!.text;
this.originalEvaluation.grade = this.evaluate!.grade.value;
this.feedbackForm.controls['published'].setValue(this.evaluate!.published);
this.feedbackForm.controls['grade'].setValue(this.evaluate!.grade.value);
this.feedbackForm.controls['text'].setValue(this.evaluate!.text);
}
return;
}));
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.submission.gradeoverby &&
this.evaluate && this.evaluate.text) {
promises.push(CoreUser.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => {
this.evaluateByProfile = profile;
return;
}));
}
if (this.assessment && !this.access.assessingallowed && this.assessment.feedbackreviewer &&
this.assessment.gradinggradeoverby) {
promises.push(CoreUser.getProfile(this.assessment.gradinggradeoverby, this.courseId, true)
.then((profile) => {
this.evaluateGradingByProfile = profile;
return;
}));
}
await Promise.all(promises);
const submissionsActions = await AddonModWorkshopOffline.getSubmissions(this.workshopId);
this.submission = await AddonModWorkshopHelper.applyOfflineData(this.submission, submissionsActions);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
} finally {
this.loaded = true;
}
}
/**
* Parse assessment to be shown.
*
* @param assessment Original assessment.
* @return Parsed assessment.
*/
protected parseAssessment(
assessment: AddonModWorkshopSubmissionAssessmentWithFormData,
): AddonModWorkshopSubmissionAssessmentWithFormData {
assessment = AddonModWorkshopHelper.realGradeValue(this.workshop, assessment);
if (this.currentUserId == assessment.reviewerid) {
this.ownAssessment = assessment;
assessment.ownAssessment = true;
}
return assessment;
}
/**
* Force leaving the page, without checking for changes.
*/
protected forceLeavePage(): void {
this.forceLeave = true;
CoreNavigator.back();
}
/**
* Check if data has changed.
*
* @return True if changed, false otherwise.
*/
protected hasEvaluationChanged(): boolean {
if (!this.loaded || !this.access.canoverridegrades) {
return false;
}
const inputData = this.feedbackForm.value;
if (this.originalEvaluation.published != inputData.published) {
return true;
}
if (this.originalEvaluation.text != inputData.text) {
return true;
}
if (this.originalEvaluation.grade != inputData.grade) {
return true;
}
return false;
}
/**
* Convenience function to refresh all the data.
*
* @return Resolved when done.
*/
protected async refreshAllData(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId));
promises.push(AddonModWorkshop.invalidateSubmissionsData(this.workshopId));
promises.push(AddonModWorkshop.invalidateSubmissionAssesmentsData(this.workshopId, this.submissionId));
if (this.assessmentId) {
promises.push(AddonModWorkshop.invalidateAssessmentFormData(this.workshopId, this.assessmentId));
promises.push(AddonModWorkshop.invalidateAssessmentData(this.workshopId, this.assessmentId));
}
if (this.assessmentUserId) {
promises.push(AddonModWorkshop.invalidateReviewerAssesmentsData(this.workshopId, this.assessmentId));
}
try {
await Promise.all(promises);
} finally {
CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, null, this.siteId);
await this.fetchSubmissionData();
}
}
/**
* Pull to refresh.
*
* @param refresher Refresher.
*/
refreshSubmission(refresher: IonRefresher): void {
if (this.loaded) {
this.refreshAllData().finally(() => {
refresher?.complete();
});
}
}
/**
* Save the assessment.
*/
async saveAssessment(): Promise<void> {
if (this.assessmentStrategy?.hasDataChanged()) {
try {
await this.assessmentStrategy.saveAssessment();
this.forceLeavePage();
} catch {
// Error, stay on the page.
}
} else {
// Nothing to save, just go back.
this.forceLeavePage();
}
}
/**
* Save the submission evaluation.
*/
async saveEvaluation(): Promise<void> {
// Check if data has changed.
if (this.hasEvaluationChanged()) {
await this.sendEvaluation();
this.forceLeavePage();
} else {
// Nothing to save, just go back.
this.forceLeavePage();
}
}
/**
* Sends the evaluation to be saved on the server.
*
* @return Resolved when done.
*/
protected async sendEvaluation(): Promise<void> {
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
const inputData: {
grade: number | string;
text: string;
published: boolean;
} = this.feedbackForm.value;
inputData.grade = inputData.grade >= 0 ? inputData.grade : '';
// Add some HTML to the message if needed.
inputData.text = CoreTextUtils.formatHtmlLines(inputData.text);
// Try to send it to server.
try {
const result = await AddonModWorkshop.evaluateSubmission(
this.workshopId,
this.submissionId,
this.courseId,
inputData.text,
inputData.published,
String(inputData.grade),
);
CoreForms.triggerFormSubmittedEvent(this.formElement, !!result, this.siteId);
await AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId).finally(() => {
const data: AddonModWorkshopSubmissionChangedEventData = {
workshopId: this.workshopId,
submissionId: this.submissionId,
};
CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
});
} catch (message) {
CoreDomUtils.showErrorModalDefault(message, 'Cannot save submission evaluation');
} finally {
modal.dismiss();
}
}
/**
* Perform the submission delete action.
*/
async deleteSubmission(): Promise<void> {
try {
await CoreDomUtils.showDeleteConfirm('addon.mod_workshop.submissiondeleteconfirm');
} catch {
return;
}
const modal = await CoreDomUtils.showModalLoading('core.deleting', true);
let success = false;
try {
await AddonModWorkshop.deleteSubmission(this.workshopId, this.submissionId, this.courseId);
success = true;
await AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Cannot delete submission');
} finally {
modal.dismiss();
if (success) {
const data: AddonModWorkshopSubmissionChangedEventData = {
workshopId: this.workshopId,
submissionId: this.submissionId,
};
CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
this.forceLeavePage();
}
}
}
/**
* Undo the submission delete action.
*
* @return Resolved when done.
*/
async undoDeleteSubmission(): Promise<void> {
await AddonModWorkshopOffline.deleteSubmissionAction(
this.workshopId,
AddonModWorkshopAction.DELETE,
).finally(async () => {
const data: AddonModWorkshopSubmissionChangedEventData = {
workshopId: this.workshopId,
submissionId: this.submissionId,
};
CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
await this.refreshAllData();
});
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
this.syncObserver?.off();
this.obsAssessmentSaved?.off();
// Restore original back functions.
CoreSync.unblockOperation(this.component, this.workshopId);
}
}
type AddonWorkshopSubmissionEvaluateData = {
published: boolean;
text: string;
grade?: CoreGradesMenuItem;
};

View File

@ -0,0 +1,159 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Type } from '@angular/core';
import { CoreDelegateHandler, CoreDelegate } from '@classes/delegate';
import { makeSingleton } from '@singletons';
import { CoreFormFields } from '@singletons/form';
import { AddonModWorkshopGetAssessmentFormDefinitionData, AddonModWorkshopGetAssessmentFormFieldsParsedData } from './workshop';
/**
* Interface that all assessment strategy handlers must implement.
*/
export interface AddonWorkshopAssessmentStrategyHandler extends CoreDelegateHandler {
/**
* The name of the assessment strategy. E.g. 'accumulative'.
*/
strategyName: string;
/**
* Return the Component to render the plugin.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @param injector Injector.
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent?(): Type<unknown>;
/**
* Prepare original values to be shown and compared.
*
* @param form Original data of the form.
* @param workshopId WorkShop Id
* @return Promise resolved with original values sorted.
*/
getOriginalValues?(
form: AddonModWorkshopGetAssessmentFormDefinitionData,
workshopId: number,
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]>;
/**
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
*
* @param originalValues Original values of the form.
* @param currentValues Current values of the form.
* @return True if data has changed, false otherwise.
*/
hasDataChanged?(
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
): boolean;
/**
* Prepare assessment data to be sent to the server depending on the strategy selected.
*
* @param currentValues Current values of the form.
* @param form Assessment form data.
* @return Promise resolved with the data to be sent. Or rejected with the input errors object.
*/
prepareAssessmentData(
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<CoreFormFields<unknown>>;
}
/**
* Delegate to register workshop assessment strategy handlers.
* You can use this service to register your own assessment strategy handlers to be used in a workshop.
*/
@Injectable({ providedIn: 'root' })
export class AddonWorkshopAssessmentStrategyDelegateService extends CoreDelegate<AddonWorkshopAssessmentStrategyHandler> {
protected handlerNameProperty = 'strategyName';
constructor() {
super('AddonWorkshopAssessmentStrategyDelegate', true);
}
/**
* Check if an assessment strategy plugin is supported.
*
* @param workshopStrategy Assessment strategy name.
* @return True if supported, false otherwise.
*/
isPluginSupported(workshopStrategy: string): boolean {
return this.hasHandler(workshopStrategy, true);
}
/**
* Get the directive to use for a certain assessment strategy plugin.
*
* @param injector Injector.
* @param workshopStrategy Assessment strategy name.
* @return The component, undefined if not found.
*/
getComponentForPlugin(workshopStrategy: string): Type<unknown> | undefined {
return this.executeFunctionOnEnabled(workshopStrategy, 'getComponent');
}
/**
* Prepare original values to be shown and compared depending on the strategy selected.
*
* @param workshopStrategy Workshop strategy.
* @param form Original data of the form.
* @param workshopId Workshop ID.
* @return Resolved with original values sorted.
*/
getOriginalValues(
workshopStrategy: string,
form: AddonModWorkshopGetAssessmentFormDefinitionData,
workshopId: number,
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'getOriginalValues', [form, workshopId]) || []);
}
/**
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
*
* @param workshopStrategy Workshop strategy.
* @param originalValues Original values of the form.
* @param currentValues Current values of the form.
* @return True if data has changed, false otherwise.
*/
hasDataChanged(
workshopStrategy: string,
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
): boolean {
return this.executeFunctionOnEnabled(workshopStrategy, 'hasDataChanged', [originalValues, currentValues]) || false;
}
/**
* Prepare assessment data to be sent to the server depending on the strategy selected.
*
* @param workshopStrategy Workshop strategy to follow.
* @param currentValues Current values of the form.
* @param form Assessment form data.
* @return Promise resolved with the data to be sent. Or rejected with the input errors object.
*/
prepareAssessmentData(
workshopStrategy: string,
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
form: AddonModWorkshopGetAssessmentFormDefinitionData,
): Promise<CoreFormFields<unknown> | undefined> {
return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'prepareAssessmentData', [currentValues, form]));
}
}
export const AddonWorkshopAssessmentStrategyDelegate = makeSingleton(AddonWorkshopAssessmentStrategyDelegateService);