Skip to content

MOBILE-4626 grades: Display grade penalties #4396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,27 @@ <h1>{{'core.gradenoun' | translate}}</h1>
<ion-item class="ion-text-wrap" *ngIf="grade.method === 'simple'">
<ion-label>
<p class="item-heading">{{ 'addon.mod_assign.currentgrade' | translate }}</p>
<p *ngIf="grade.gradebookGrade && !grade.scale">
{{ grade.gradebookGrade }}
<p class="core-grading-summary-grade">
@if (grade.gradebookGrade) {
@if (grade.penalty) {
<ion-icon name="fas-triangle-exclamation" color="danger" aria-hidden="true" />
}
<span>
@if (grade.scale) {
{{ grade.scale[grade.gradebookGrade].label }}
} @else {
{{ grade.gradebookGrade }}
}
</span>
} @else {
-
}
</p>
<p *ngIf="grade.gradebookGrade && grade.scale">
{{ grade.scale[grade.gradebookGrade].label }}
</p>
<p *ngIf="!grade.gradebookGrade">-</p>
@if (grade.penalty) {
<p class="core-grading-summary-penalty">
{{ grade.penalty }}
</p>
}
</ion-label>
</ion-item>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
:host {

.core-grading-summary-grade {
display: flex;
align-items: center;
gap: 0.25rem;

ion-icon {
flex-shrink: 0;
}

.penalty-indicator-icon {
display: none;
}
}

.core-grading-summary-penalty {
color: var(--danger);
font-size: 0.75em;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { CoreFormFields, CoreForms } from '@singletons/form';
@Component({
selector: 'addon-mod-assign-edit-feedback-modal',
templateUrl: 'edit-feedback-modal.html',
styleUrl: 'edit-feedback-modal.scss',
standalone: true,
imports: [
CoreSharedModule,
Expand Down Expand Up @@ -359,6 +360,8 @@ export class AddonModAssignEditFeedbackModalComponent implements OnDestroy, OnIn
});
gradeInfo.disabled = grade.gradeislocked || grade.gradeisoverridden;
}

this.grade.penalty = CoreGradesHelper.getPenaltyFromGrade(grade.gradeformatted);
});

gradeInfo.outcomes = outcomes;
Expand Down Expand Up @@ -599,6 +602,7 @@ type AddonModAssignSubmissionGrade = {
lang: string;
disabled: boolean;
unreleasedGrade?: SafeNumber | string;
penalty?: string; // Parsed from grade.
};

type AddonModAssignGradeInfo = Omit<CoreCourseModuleGradeInfo, 'outcomes'> & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,19 @@ <h2 class="big">{{'addon.mod_assign.feedback' | translate}}</h2>
<ion-item class="ion-text-wrap core-grading-summary">
<ion-label>
<p class="item-heading">{{ 'core.gradenoun' | translate }}</p>
<p>
<core-format-text [text]="feedback.gradefordisplay" [filter]="false" />
<p class="core-grading-summary-grade">
@if (feedback.penalty) {
<ion-icon name="fas-triangle-exclamation" color="danger" aria-hidden="true" />
}
<span>
<core-format-text [text]="feedback.gradefordisplay" [filter]="false" />
</span>
</p>
@if (feedback.penalty) {
<p class="core-grading-summary-penalty">
{{ feedback.penalty }}
</p>
}
</ion-label>
@if (feedback.advancedgrade) {
<ion-button slot="end" (click)="showAdvancedGrade(feedback.gradefordisplay)"
Expand Down Expand Up @@ -365,9 +375,17 @@ <h4 class="big">{{'core.gradenoun' | translate}}</h4>
<ion-item class="ion-text-wrap core-grading-summary">
<ion-label>
<p class="item-heading">{{ 'core.gradenoun' | translate }}</p>
<p>
<p class="core-grading-summary-grade">
@if (attempt.penalty) {
<ion-icon name="fas-triangle-exclamation" color="danger" aria-hidden="true" />
}
<core-format-text [text]="attempt.grade.gradefordisplay" [filter]="false" />
</p>
@if (attempt.penalty) {
<p class="core-grading-summary-penalty">
{{ attempt.penalty }}
</p>
}
</ion-label>
@if (attempt.advancedgrade) {
<ion-button slot="end" (click)="showAdvancedGrade(attempt.grade.gradefordisplay)"
Expand Down
19 changes: 19 additions & 0 deletions src/addons/mod/assign/components/submission/submission.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@
display: none;
}

.core-grading-summary-grade {
display: flex;
align-items: center;
gap: 0.25rem;

ion-icon {
flex-shrink: 0;
}

.penalty-indicator-icon {
display: none;
}
}

.core-grading-summary-penalty {
color: var(--danger);
font-size: 0.75em;
}

ion-badge {
margin-left: 2px;
margin-right: 2px;
Expand Down
4 changes: 4 additions & 0 deletions src/addons/mod/assign/components/submission/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
// If we have data about the grader, get its profile.
attempt.grader = await this.getGrader(attempt.grade);
attempt.advancedgrade = this.getAdvancedGrade(attempt.grade?.gradefordisplay);
attempt.penalty = CoreGradesHelper.getPenaltyFromGrade(attempt.grade?.gradefordisplay);
});

promises.push(...graderPromises);
Expand Down Expand Up @@ -596,6 +597,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {

// Check if the grade uses advanced grading.
this.feedback.advancedgrade = this.getAdvancedGrade(feedback.gradefordisplay);
this.feedback.penalty = CoreGradesHelper.getPenaltyFromGrade(feedback.gradefordisplay);
}

// Get the grade for the assign.
Expand Down Expand Up @@ -1042,12 +1044,14 @@ type AddonModAssignSubmissionAttemptFormatted = AddonModAssignSubmissionAttempt
*/
type AddonModAssignSubmissionFeedbackFormatted = AddonModAssignSubmissionFeedback & {
advancedgrade?: boolean; // Calculated in the app. Whether it uses advanced grading.
penalty?: string; // Parsed from gradefordisplay.
};

type AddonModAssignSubmissionPreviousAttemptFormatted = AddonModAssignSubmissionPreviousAttempt & {
submissionStatusBadge?: StatusBadge;
grader?: CoreUserProfile;
advancedgrade?: boolean;
penalty?: string; // Parsed from gradefordisplay.
};

type StatusBadge = {
Expand Down
68 changes: 68 additions & 0 deletions src/addons/mod/assign/tests/behat/grade_penalties.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@addon_mod_assign @app @mod @mod_assign @javascript @lms_from5.0
Feature: Grade penalties in the assignment activity

Background:
Given the Moodle site is compatible with this feature
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | [email protected] |
| student1 | Student | 1 | [email protected] |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I enable grade penalties for assignment
And the following "activity" exists:
| activity | assign |
| course | C1 |
| name | Test assignment name |
| intro | Test assignment description |
| grade | 100 |
| duedate | ##yesterday## |
| gradepenalty | 1 |
| assignsubmission_onlinetext_enabled | 1 |
| submissiondrafts | 0 |
| maxattempts | -1 |
| attemptreopenmethod | manual |
# Add a submission.
And the following "mod_assign > submissions" exist:
| assign | user | onlinetext |
| Test assignment name | student1 | I'm the student first submission |
And I am on the "Test assignment name" Activity page logged in as teacher1
And I go to "Student 1" "Test assignment name" activity advanced grading page
And I set the following fields to these values:
| Grade out of 100 | 50 |
| Notify student | 0 |
| Allow another attempt | 1 |
And I press "Save changes"
And I log out

Scenario: View submission with grade penalty as student
Given I entered the assign activity "Test assignment name" on course "Course 1" as "student1" in the app
When I press "Attempt 1" in the app
Then I should find "Late penalty applied -10.00 marks" within "Feedback" "ion-card" in the app
And I should find "Late penalty applied -10.00 marks" within "Attempt 1" "ion-accordion" in the app

Scenario: View activity summary with grade penalty as student
Given I entered the assign activity "Test assignment name" on course "Course 1" as "student1" in the app
When I press "Information" "ion-button" in the app
And I press "Grade" "ion-item" in the app
Then I should find "Late penalty applied -10.00 marks" within "Gradebook" "ion-card" in the app

Scenario: View submission with grade penalty as teacher
Given I entered the assign activity "Test assignment name" on course "Course 1" as "teacher1" in the app
When I press "Participants" in the app
And I press "Student 1" in the app
And I press "Attempt 1" in the app
Then I should find "Late penalty applied -10.00 marks" within "Feedback" "ion-card" in the app
And I should find "Late penalty applied -10.00 marks" within "Attempt 1" "ion-accordion" in the app

Scenario: Edit feedback with grade penalty as teacher
Given I entered the assign activity "Test assignment name" on course "Course 1" as "teacher1" in the app
When I press "Participants" in the app
And I press "Student 1" in the app
And I press "Grade" "ion-button" in the app
Then I should find "Late penalty applied -10.00 marks" in the app
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,34 @@ <h2>
<ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon"
[class.expandable-status-icon-expanded]="grade.expanded" />
<ion-label>
<p class="item-heading" *ngIf="!grade.itemmodule">
<core-format-text [text]="grade.gradeitem" contextLevel="course" [contextInstanceId]="courseId" />
<p class="item-heading">
@if (grade.itemmodule) {
{{ 'core.gradenoun' | translate }}
} @else {
<core-format-text [text]="grade.gradeitem" contextLevel="course" [contextInstanceId]="courseId" />
}
</p>
<p class="item-heading" *ngIf="grade.itemmodule">
{{ 'core.gradenoun' | translate }}
</p>
<p *ngIf="grade.grade && grade.grade !== '-'" [innerHTML]="grade.grade"></p>
<ion-badge *ngIf="!grade.grade || grade.grade === '-'" color="light">
{{ 'core.grades.notgraded' | translate }}
</ion-badge>
@if (grade.grade && grade.grade !== '-') {
<p class="core-grading-summary-grade">
@if (grade.penalty) {
<ion-icon name="fas-triangle-exclamation" color="danger" aria-hidden="true" />
}
<span [innerHTML]="grade.grade"></span>
</p>
} @else {
<ion-badge color="light">
{{ 'core.grades.notgraded' | translate }}
</ion-badge>
}
@if (grade.penalty) {
<p class="core-grading-summary-penalty" [class.sr-only]="!grade.expanded">
{{ grade.penalty }}
</p>
}
</ion-label>
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="end" [attr.aria-label]="grade.iconAlt" />
<img *ngIf="grade.image && !grade.itemmodule" [url]="grade.image" slot="end" [alt]="grade.iconAlt"
core-external-content />
<ion-icon *ngIf="grade.image && grade.itemmodule" name="fas-chart-bar" slot="end"
[attr.aria-label]="grade.iconAlt" />
</ion-item>
<div *ngIf="grade.expanded" [id]="'grade-'+grade.id">
<ion-item class="ion-text-wrap" *ngIf="grade.weight?.length > 0 && grade.weight !== '-'">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
@use "theme/globals" as *;

:host {
.core-grading-summary-grade {
display: flex;
align-items: center;

ion-icon {
flex-shrink: 0;
}

::ng-deep .penalty-indicator-icon {
display: none;
}
}

.core-grading-summary-penalty {
color: var(--danger);
font-size: 0.75em;
}
}

:host ::ng-deep ion-item[collapsible] ion-label {
margin-top: 12px;
}
Expand Down
30 changes: 26 additions & 4 deletions src/core/features/grades/pages/course/course.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,21 @@ <h1>{{ title }}</h1>
} @else if (column.name === 'grade') {
<td [class.ion-hide-md-down]="column.hiddenPhone"
class="ion-text-start core-grades-table-grade {{row.gradeClass}}">
@if (row.gradeIcon) {
<ion-icon [name]="row.gradeIcon" [attr.aria-label]="row.gradeIconAlt" />
<div>
@if (row.gradeIcon) {
<ion-icon [name]="row.gradeIcon" [attr.aria-label]="row.gradeIconAlt" />
}
@if (row.penalty) {
<ion-icon name="fas-triangle-exclamation" color="danger"
aria-hidden="true" />
}
<span [innerHTML]="row[column.name]"></span>
</div>
@if (row.penalty) {
<div class="core-grades-expanded-grade-penalty" [class.sr-only]="showSummary">
{{ row.penalty }}
</div>
}
<span [innerHTML]="row[column.name]"></span>
</td>
} @else if (column.name !== 'gradeitem' && column.name !== 'feedback' &&
column.name !== 'grade' && row[column.name] !== undefined) {
Expand Down Expand Up @@ -112,7 +123,18 @@ <h1>{{ title }}</h1>
<ion-item class="ion-text-wrap">
<ion-label>
<p class="item-heading">{{ 'core.gradenoun' | translate}}</p>
<p [innerHTML]="row.grade"></p>
<p class="core-grades-expanded-grade">
@if (row.penalty) {
<ion-icon name="fas-triangle-exclamation" color="danger"
aria-hidden="true" />
}
<span [innerHTML]="row.grade"></span>
</p>
@if (row.penalty) {
<p class="core-grades-expanded-grade-penalty">
{{ row.penalty }}
</p>
}
</ion-label>
</ion-item>
}
Expand Down
Loading