diff --git a/grade/grading/form/rubric/edit.php b/grade/grading/form/rubric/edit.php
index 71f47b88ef1..c1849f2a268 100644
--- a/grade/grading/form/rubric/edit.php
+++ b/grade/grading/form/rubric/edit.php
@@ -56,8 +56,8 @@ if ($mform->is_cancelled()) {
// If we do not go back to management url and the minscore warning needs to be displayed, display it during redirection.
$warning = null;
- if (!empty($data->returnurl)) {
- if (($scores = $controller->get_min_max_score()) && $scores['minscore'] <> 0) {
+ if (!empty($data->returnurl) && $data->returnurl !== $manager->get_management_url()->out(false)) {
+ if (empty($data->rubric['options']['lockzeropoints']) && ($scores = $controller->get_min_max_score()) && $scores['minscore'] <> 0) {
$warning = get_string('zerolevelsabsent', 'gradingform_rubric').'
'.
html_writer::link($manager->get_management_url(), get_string('back'));
}
diff --git a/grade/grading/form/rubric/lang/en/gradingform_rubric.php b/grade/grading/form/rubric/lang/en/gradingform_rubric.php
index dacd5ddaf58..bb36c3ee5a5 100644
--- a/grade/grading/form/rubric/lang/en/gradingform_rubric.php
+++ b/grade/grading/form/rubric/lang/en/gradingform_rubric.php
@@ -45,7 +45,7 @@ $string['err_nocriteria'] = 'Rubric must contain at least one criterion';
$string['err_nodefinition'] = 'Level definition can not be empty';
$string['err_nodescription'] = 'Criterion description can not be empty';
$string['err_novariations'] = 'Criterion levels cannot all be worth the same number of points';
-$string['err_scoreformat'] = 'Number of points for each level must be a valid non-negative number';
+$string['err_scoreformat'] = 'Number of points for each level must be a valid number';
$string['err_totalscore'] = 'Maximum number of points possible when graded by the rubric must be more than zero';
$string['gradingof'] = '{$a} grading';
$string['level'] = 'Level {$a->definition}, {$a->score} points.';
@@ -53,6 +53,11 @@ $string['leveldelete'] = 'Delete level {$a}';
$string['leveldefinition'] = 'Level {$a} definition';
$string['levelempty'] = 'Click to edit level';
$string['levelsgroup'] = 'Levels group';
+$string['lockzeropoints'] = 'When converting rubric score to points/scale assume that minimum number of points is 0';
+$string['lockzeropoints_help'] = 'Locking the minimum number of points will allow to create rubrics without 0-levels. This may also mean that 0% grade on this rubric is not possible to achieve.
+The maximum score in the rubric is always converted to the maximum grade.
+When this setting is unchecked, the minimum possible score for this rubric will be converted to the minimum grade available in the module (which is zero unless the scale is used).
+Explanation of rubrics grade calculation';
$string['name'] = 'Name';
$string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.';
$string['pluginname'] = 'Rubric';
@@ -66,8 +71,9 @@ $string['rubric'] = 'Rubric';
$string['rubricmapping'] = 'Score to grade mapping rules';
$string['rubricmappingexplained'] = 'The minimum possible score for this rubric is {$a->minscore} points and it will be converted to the minimum grade available in this module (which is zero unless the scale is used).
The maximum score {$a->maxscore} points will be converted to the maximum grade.
- Intermediate scores will be converted respectively and rounded to the nearest available grade.
- If a scale is used instead of a grade, the score will be converted to the scale elements as if they were consecutive integers.';
+ Intermediate scores will be converted respectively.
+ If a scale is used for grading, the score will be rounded and converted to the scale elements as if they were consecutive integers.
+ You can change the method of grade calculation in the Rubic options section below.';
$string['rubricnotcompleted'] = 'Please choose something for each criterion';
$string['rubricoptions'] = 'Rubric options';
$string['rubricstatus'] = 'Current rubric status';
diff --git a/grade/grading/form/rubric/lib.php b/grade/grading/form/rubric/lib.php
index 9504b42b005..39af91091c7 100644
--- a/grade/grading/form/rubric/lib.php
+++ b/grade/grading/form/rubric/lib.php
@@ -143,8 +143,16 @@ class gradingform_rubric_controller extends gradingform_controller {
// reload the definition from the database
$currentdefinition = $this->get_definition(true);
- // update rubric data
$haschanges = array();
+
+ // Check if 'lockzeropoints' option has changed.
+ $newlockzeropoints = $newdefinition->rubric['options']['lockzeropoints'];
+ $currentoptions = $this->get_options();
+ if ((bool)$newlockzeropoints != (bool)$currentoptions['lockzeropoints']) {
+ $haschanges[3] = true;
+ }
+
+ // update rubric data
if (empty($newdefinition->rubric['criteria'])) {
$newcriteria = array();
} else {
@@ -203,10 +211,7 @@ class gradingform_rubric_controller extends gradingform_controller {
}
foreach ($levelsdata as $levelid => $level) {
if (isset($level['score'])) {
- $level['score'] = (float)$level['score'];
- if ($level['score']<0) {
- $level['score'] = 0;
- }
+ $level['score'] = unformat_float($level['score']);
}
if (preg_match('/^NEWID\d+$/', $levelid)) {
// insert level into DB
@@ -354,6 +359,7 @@ class gradingform_rubric_controller extends gradingform_controller {
public static function get_default_options() {
$options = array(
'sortlevelsasc' => 1,
+ 'lockzeropoints' => 1,
'alwaysshowdefinition' => 1,
'showdescriptionteacher' => 1,
'showdescriptionstudent' => 1,
@@ -368,6 +374,9 @@ class gradingform_rubric_controller extends gradingform_controller {
/**
* Gets the options of this rubric definition, fills the missing options with default values
*
+ * The only exception is 'lockzeropoints' - if other options are present in the json string but this
+ * one is absent, this means that the rubric was created before Moodle 3.2 and the 0 value should be used.
+ *
* @return array
*/
public function get_options() {
@@ -377,6 +386,11 @@ class gradingform_rubric_controller extends gradingform_controller {
foreach ($thisoptions as $option => $value) {
$options[$option] = $value;
}
+ if (!array_key_exists('lockzeropoints', $thisoptions)) {
+ // Rubrics created before Moodle 3.2 don't have 'lockzeropoints' option. In this case they should not
+ // assume default value 1 but use "legacy" value 0.
+ $options['lockzeropoints'] = 0;
+ }
}
return $options;
}
@@ -522,7 +536,10 @@ class gradingform_rubric_controller extends gradingform_controller {
$rubric .= $output->box($this->get_formatted_description(), 'gradingform_rubric-description');
}
if (has_capability('moodle/grade:managegradingforms', $page->context)) {
- $rubric .= $output->display_rubric_mapping_explained($this->get_min_max_score());
+ if (!$options['lockzeropoints']) {
+ // Warn about using grade calculation method where minimum number of points is flexible.
+ $rubric .= $output->display_rubric_mapping_explained($this->get_min_max_score());
+ }
$rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric');
} else {
$rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW_GRADED, 'rubric');
@@ -889,11 +906,19 @@ class gradingform_rubric_instance extends gradingform_instance {
foreach ($grade['criteria'] as $id => $record) {
$curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score'];
}
- $gradeoffset = ($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*($maxgrade-$mingrade);
- if ($this->get_controller()->get_allow_grade_decimals()) {
- return $gradeoffset + $mingrade;
+
+ $allowdecimals = $this->get_controller()->get_allow_grade_decimals();
+ $options = $this->get_controller()->get_options();
+
+ if ($options['lockzeropoints']) {
+ // Grade calculation method when 0-level is locked.
+ $grade = max($mingrade, $curscore / $scores['maxscore'] * $maxgrade);
+ return $allowdecimals ? $grade : round($grade, 0);
+ } else {
+ // Alternative grade calculation method.
+ $gradeoffset = ($curscore - $scores['minscore']) / ($scores['maxscore'] - $scores['minscore']) * ($maxgrade - $mingrade);
+ return ($allowdecimals ? $gradeoffset : round($gradeoffset, 0)) + $mingrade;
}
- return round($gradeoffset, 0) + $mingrade;
}
/**
diff --git a/grade/grading/form/rubric/renderer.php b/grade/grading/form/rubric/renderer.php
index dbaa6661f14..4b510d4c507 100644
--- a/grade/grading/form/rubric/renderer.php
+++ b/grade/grading/form/rubric/renderer.php
@@ -470,6 +470,9 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
$html .= html_writer::tag('label', get_string($option, 'gradingform_rubric'), array('for' => $attrs['id']));
break;
}
+ if (get_string_manager()->string_exists($option.'_help', 'gradingform_rubric')) {
+ $html .= $this->help_icon($option, 'gradingform_rubric');
+ }
$html .= html_writer::end_tag('div'); // .option
}
$html .= html_writer::end_tag('div'); // .options
@@ -636,10 +639,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
if ($scores['minscore'] <> 0) {
$html .= $this->output->notification(get_string('zerolevelsabsent', 'gradingform_rubric'), 'error');
}
- $html .= $this->box(
- html_writer::tag('h4', get_string('rubricmapping', 'gradingform_rubric')).
- html_writer::tag('div', get_string('rubricmappingexplained', 'gradingform_rubric', (object)$scores))
- , 'generalbox rubricmappingexplained');
+ $html .= $this->output->notification(get_string('rubricmappingexplained', 'gradingform_rubric', (object)$scores), 'info');
return $html;
}
}
diff --git a/grade/grading/form/rubric/rubriceditor.php b/grade/grading/form/rubric/rubriceditor.php
index 49b0193446f..3d64b5fd5af 100644
--- a/grade/grading/form/rubric/rubriceditor.php
+++ b/grade/grading/form/rubric/rubriceditor.php
@@ -232,29 +232,30 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
'score' => 0,
);
foreach ($criterion['levels'] as $lastlevel) {
- if (isset($lastlevel['score']) && $level['score'] < $lastlevel['score'] + 1) {
- $level['score'] = $lastlevel['score'] + 1;
+ if (isset($lastlevel['score'])) {
+ $level['score'] = max($level['score'], ceil(unformat_float($lastlevel['score'])) + 1);
}
}
$this->nonjsbuttonpressed = true;
}
if (!array_key_exists('delete', $level)) {
+ $score = unformat_float($level['score'], true);
if ($withvalidation) {
if (!strlen(trim($level['definition']))) {
$errors['err_nodefinition'] = 1;
$level['error_definition'] = true;
}
- if (!preg_match('#^[\+]?\d*$#', trim($level['score'])) && !preg_match('#^[\+]?\d*[\.,]\d+$#', trim($level['score']))) {
+ if ($score === null || $score === false) {
$errors['err_scoreformat'] = 1;
$level['error_score'] = true;
}
}
$levels[$levelid] = $level;
- if ($minscore === null || (float)$level['score'] < $minscore) {
- $minscore = (float)$level['score'];
+ if ($minscore === null || $score < $minscore) {
+ $minscore = $score;
}
- if ($maxscore === null || (float)$level['score'] > $maxscore) {
- $maxscore = (float)$level['score'];
+ if ($maxscore === null || $score > $maxscore) {
+ $maxscore = $score;
}
} else {
$this->nonjsbuttonpressed = true;
@@ -313,8 +314,10 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
// create validation error string (if needed)
if ($withvalidation) {
- if ($overallminscore == $overallmaxscore) {
- $errors['err_novariations'] = 1;
+ if (!$return['options']['lockzeropoints']) {
+ if ($overallminscore == $overallmaxscore) {
+ $errors['err_novariations'] = 1;
+ }
}
if (count($errors)) {
$rv = array();
diff --git a/grade/grading/form/rubric/tests/behat/edit_rubric.feature b/grade/grading/form/rubric/tests/behat/edit_rubric.feature
index 232e9959e96..933b22ba027 100644
--- a/grade/grading/form/rubric/tests/behat/edit_rubric.feature
+++ b/grade/grading/form/rubric/tests/behat/edit_rubric.feature
@@ -58,7 +58,7 @@ Feature: Rubrics can be created and edited
And I complete the advanced grading form with these values:
| Feedback comments | In general... work harder... |
# Checking that the user grade is correct.
- And I should see "58.33" in the "Student 1" "table_row"
+ And I should see "65" in the "Student 1" "table_row"
# Updating the user grade.
And I go to "Student 1" "Test assignment 1 name" activity advanced grading page
And I grade by filling the rubric with:
@@ -68,13 +68,13 @@ Feature: Rubrics can be created and edited
#And the level with "50" points was previously selected for the rubric criterion "Criterion 1"
#And the level with "20" points is selected for the rubric criterion "Criterion 1"
And I save the advanced grading form
- And I should see "22.62" in the "Student 1" "table_row"
+ And I should see "35" in the "Student 1" "table_row"
And I log out
# Viewing it as a student.
And I log in as "student1"
And I follow "Course 1"
And I follow "Test assignment 1 name"
- And I should see "22.62" in the ".feedback" "css_element"
+ And I should see "35" in the ".feedback" "css_element"
And I should see "Rubric test description" in the ".feedback" "css_element"
And I should see "In general... work harder..."
And the level with "10" points is selected for the rubric criterion "Criterion 2"
@@ -97,7 +97,7 @@ Feature: Rubrics can be created and edited
And I log in as "student1"
And I follow "Course 1"
And I follow "Test assignment 1 name"
- And I should see "22.62" in the ".feedback" "css_element"
+ And I should see "35" in the ".feedback" "css_element"
And the level with "20" points is selected for the rubric criterion "Criterion 1"
And I log out
# Editing a rubric with significant changes.
@@ -105,7 +105,7 @@ Feature: Rubrics can be created and edited
And I follow "Course 1"
And I go to "Test assignment 1 name" advanced grading definition page
And I click on "Move down" "button" in the "Criterion 2" "table_row"
- And I replace "1" rubric level with "11" in "Criterion 1" criterion
+ And I replace "1" rubric level with "60" in "Criterion 1" criterion
And I press "Save"
And I should see "You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded."
And I press "Continue"
@@ -114,7 +114,7 @@ Feature: Rubrics can be created and edited
And I log in as "student1"
And I follow "Course 1"
And I follow "Test assignment 1 name"
- And I should see "22.62" in the ".feedback" "css_element"
+ And I should see "35" in the ".feedback" "css_element"
And the level with "20" points is not selected for the rubric criterion "Criterion 1"
And I log out
# Regrade student.
@@ -129,7 +129,7 @@ Feature: Rubrics can be created and edited
And I log in as "student1"
And I follow "Course 1"
And I follow "Test assignment 1 name"
- And I should see "12.16" in the ".feedback" "css_element"
+ And I should see "31.82" in the ".feedback" "css_element"
And the level with "20" points is not selected for the rubric criterion "Criterion 1"
# Hide all rubric info for students
And I log out
diff --git a/grade/grading/form/rubric/tests/behat/grade_calculation.feature b/grade/grading/form/rubric/tests/behat/grade_calculation.feature
new file mode 100644
index 00000000000..b9ca4224281
--- /dev/null
+++ b/grade/grading/form/rubric/tests/behat/grade_calculation.feature
@@ -0,0 +1,56 @@
+@gradingform @gradingform_rubric @javascript
+Feature: Converting rubric score to grades
+ In order to use and refine rubrics to grade students
+ As a teacher
+ I need to be able to use different grade settings
+
+ Scenario Outline:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | format |
+ | Course 1 | C1 | topics |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ And the following "scales" exist:
+ | name | scale |
+ | Test scale 1 | Disappointing, Good, Very good, Excellent |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber | grade | advancedgradingmethod_submissions |
+ | assign | Test assignment 1 | Test | C1 | assign1 | | rubric |
+ When I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I go to "Test assignment 1" advanced grading definition page
+ And I set the following fields to these values:
+ | Name | Assignment 1 rubric |
+ | Description | Rubric test description |
+ | When converting rubric score to points/scale assume that minimum number of points is 0 | |
+ And I define the following rubric:
+ | Criterion 1 | Level 11 | 20 | Level 12 | 25 | Level 13 | 40 | Level 14 | 50 |
+ | Criterion 2 | Level 21 | 20 | Level 22 | 25 | Level 23 | 30 | | |
+ | Criterion 3 | Level 31 | 10 | Level 32 | 20 | | | | |
+ And I press "Save rubric and make it ready"
+ # Grading a student.
+ And I go to "Student 1" "Test assignment 1" activity advanced grading page
+ And I grade by filling the rubric with:
+ | Criterion 1 | 25 | |
+ | Criterion 2 | 20 | |
+ | Criterion 3 | 10 | |
+ And I save the advanced grading form
+ # Checking that the user grade is correct.
+ And I should see "" in the "student1@example.com" "table_row"
+ And I log out
+
+ Examples:
+ | grade | lockzeropoints | studentgrade |
+ | 100 | 1 | 55.00 |
+ | 70 | 1 | 38.50 |
+ | Test scale 1 | 1 | Good |
+ | 100 | | 10.00 |
+ | 70 | | 7.00 |
+ | Test scale 1 | | Disappointing |
diff --git a/grade/grading/form/rubric/tests/behat/negative_points.feature b/grade/grading/form/rubric/tests/behat/negative_points.feature
new file mode 100644
index 00000000000..83ee60b9f8e
--- /dev/null
+++ b/grade/grading/form/rubric/tests/behat/negative_points.feature
@@ -0,0 +1,65 @@
+@gradingform @gradingform_rubric @javascript
+Feature: Rubrics can have levels with negative scores
+ In order to use and refine rubrics to grade students
+ As a teacher
+ I need to be able to penalise for very wrong submissions
+
+ Scenario: Using negative levels in rubrics
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+ | student3 | Student | 3 | student3@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | format |
+ | Course 1 | C1 | topics |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ | student3 | C1 | student |
+ And the following "scales" exist:
+ | name | scale |
+ | Test scale 1 | Disappointing, Good, Very good, Excellent |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber | grade | advancedgradingmethod_submissions |
+ | assign | Test assignment 1 | Test | C1 | assign1 | 100 | rubric |
+ When I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I go to "Test assignment 1" advanced grading definition page
+ And I set the following fields to these values:
+ | Name | Assignment 1 rubric |
+ | Description | Rubric test description |
+ And I define the following rubric:
+ | Criterion 1 | Did not try | -11 | Level 12 | 25 | Level 13 | 40 | Level 14 | 50 |
+ | Criterion 2 | Very bad | -20 | Level 22 | 25 | Level 23 | 30 | | |
+ | Criterion 3 | Level 31 | 10 | Level 32 | 20 | | | | |
+ And I press "Save rubric and make it ready"
+ # Grading a student.
+ And I go to "Student 1" "Test assignment 1" activity advanced grading page
+ And I grade by filling the rubric with:
+ | Criterion 1 | 25 | |
+ | Criterion 2 | 30 | |
+ | Criterion 3 | 10 | |
+ And I save the advanced grading form
+ And I go to "Student 2" "Test assignment 1" activity advanced grading page
+ And I grade by filling the rubric with:
+ | Criterion 1 | 25 | |
+ | Criterion 2 | -20 | |
+ | Criterion 3 | 10 | |
+ And I save the advanced grading form
+ And I go to "Student 3" "Test assignment 1" activity advanced grading page
+ And I grade by filling the rubric with:
+ | Criterion 1 | -11 | |
+ | Criterion 2 | -20 | |
+ | Criterion 3 | 10 | |
+ And I save the advanced grading form
+ # Checking that the user grade is correct.
+ And I should see "65.00" in the "student1@example.com" "table_row"
+ And I should see "15.00" in the "student2@example.com" "table_row"
+ And I should see "0.00" in the "student3@example.com" "table_row"
+ And I should not see "-" in the "student3@example.com" "table_row"
+ And I log out