]*>' . get_string('summaryofattempts', 'quiz') . '<\/caption>/';
+
+ // Check caption existed.
+ $this->assertMatchesRegularExpression($captionpattern, $table);
+ // Check column attempt.
+ $this->assertMatchesRegularExpression('/]*>' . $attempt->get_attempt_number() . '<\/td>/', $table);
+ // Check column state.
+ $this->assertMatchesRegularExpression('/ | ]*>' . ucfirst($attempt->get_state()) . '.+?<\/td>/', $table);
+ // Check column marks.
+ $this->assertMatchesRegularExpression('/ | ]* c2.+?' .
+ quiz_format_grade($quiz, $attempt->get_sum_marks()) .'<\/td>/', $table);
+ // Check column grades.
+ $this->assertMatchesRegularExpression('/ | ]* c2.+?0\.00<\/td>/', $table);
+ // Check column review.
+ $this->assertMatchesRegularExpression('/ | ]*>.+?Review<\/a><\/td>/', $table);
+ }
}
diff --git a/mod/quiz/tests/behat/flag_questions.feature b/mod/quiz/tests/behat/flag_questions.feature
new file mode 100644
index 0000000000000..29a415d41c0c5
--- /dev/null
+++ b/mod/quiz/tests/behat/flag_questions.feature
@@ -0,0 +1,72 @@
+@mod @mod_quiz
+Feature: Flag quiz questions
+ As a student
+ In order to flag a quiz questions
+ All review options for immediately after the attempt are ticked
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@email.com |
+ | teacher1 | Teacher | 1 | teacher1@email.com |
+ And the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | C1 | student |
+ | teacher1 | C1 | teacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | questiontext |
+ | Test questions | truefalse | TF1 | First question |
+ | Test questions | truefalse | TF2 | Second question |
+ | Test questions | truefalse | TF3 | Third question |
+ And the following "activity" exists:
+ | activity | quiz |
+ | name | Quiz 1 |
+ | course | C1 |
+ | attemptimmediately | 1 |
+ | correctnessimmediately | 1 |
+ | maxmarksimmediately | 1 |
+ | marksimmediately | 1 |
+ | specificfeedbackimmediately | 1 |
+ | generalfeedbackimmediately | 1 |
+ | rightanswerimmediately | 1 |
+ | overallfeedbackimmediately | 1 |
+ And quiz "Quiz 1" contains the following questions:
+ | question | page |
+ | TF1 | 1 |
+ | TF2 | 2 |
+ | TF3 | 3 |
+
+ @javascript
+ Scenario: Flag a quiz during and after quiz attempt
+ Given I am on the "Quiz 1" "quiz activity" page logged in as student1
+ And I press "Attempt quiz"
+ # flag question 1
+ When I press "Flag question"
+ # Confirm question 1 is flagged in navigation
+ Then "Question 1 This page Flagged" "link" should exist
+ # Confirm that link in question 1 is changed to Remove flag
+ And I should see "Remove flag" in the "First question" "question"
+ # Answer questions
+ And I click on "True" "radio" in the "First question" "question"
+ And I press "Next page"
+ And I click on "True" "radio" in the "Second question" "question"
+ And I press "Next page"
+ And I click on "True" "radio" in the "Third question" "question"
+ And I follow "Finish attempt ..."
+ And I press "Submit all and finish"
+ And I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue"
+ # Confirm only flagged question is flagged
+ And I should see "Remove flag" in the "First question" "question"
+ And I should see "Flag question" in the "Second question" "question"
+ And I should see "Flag question" in the "Third question" "question"
+ And I click on "Flagged" "button" in the "Second question" "question"
+ And I am on the "Quiz 1" "mod_quiz > Grades report" page logged in as teacher1
+ And "Flagged" "icon" should exist in the "Student 1" "table_row"
+ And I am on the "Quiz 1" "mod_quiz > Responses report" page
+ And "Flagged" "icon" should exist in the "Student 1" "table_row"
diff --git a/mod/quiz/tests/behat/quiz_activity_certainty.feature b/mod/quiz/tests/behat/quiz_activity_certainty.feature
new file mode 100644
index 0000000000000..c8de86c95608f
--- /dev/null
+++ b/mod/quiz/tests/behat/quiz_activity_certainty.feature
@@ -0,0 +1,96 @@
+@mod @mod_quiz
+Feature: Set a quiz with certainty-based marking
+ As a teacher
+ In order to set a a quiz with certainty-based marking
+ I should set question behaviour to "Immediate feedback with CBM"
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | One | student1@example.com |
+ | teacher1 | Teacher | One | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | C1 | student |
+ | teacher1 | C1 | editingteacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | questiontext |
+ | Test questions | truefalse | TF1 | First question |
+ | Test questions | truefalse | TF2 | Second question |
+ | Test questions | truefalse | TF3 | Third question |
+ And the following "activities" exist:
+ | activity | name | course |
+ | quiz | Quiz 1 | C1 |
+ And quiz "Quiz 1" contains the following questions:
+ | question | page |
+ | TF1 | 1 |
+ | TF2 | 2 |
+ | TF3 | 3 |
+
+ @javascript
+ Scenario: Teacher can set a quiz with certainty-based marking
+ Given I am on the "Quiz 1" "quiz activity editing" page logged in as teacher1
+ And I set the following fields to these values:
+ | preferredbehaviour | immediatecbm |
+ And I press "Save and return to course"
+ And I am on the "Quiz 1" "quiz activity" page logged in as student1
+ When I press "Attempt quiz"
+ # Press "Check" without selecting a certainty
+ And I press "Check"
+ # Confirm that "Please select a certainty." is displayed when "Check" is pressed
+ Then I should see "Please select a certainty."
+ And I click on "True" "radio" in the "First question" "question"
+ And I click on "C=1 (Unsure: <67%)" "radio" in the "First question" "question"
+ And I press "Next page"
+ And I click on "False" "radio" in the "Second question" "question"
+ And I click on "C=2 (Mid: >67%)" "radio" in the "Second question" "question"
+ And I press "Next page"
+ And I click on "True" "radio" in the "Third question" "question"
+ And I click on "C=3 (Quite sure: >80%)" "radio" in the "Third question" "question"
+ And I press "Finish attempt ..."
+ And I press "Submit all and finish"
+ And I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue"
+ # As student1, confirm the results of own attempt
+ And the following should exist in the "quizreviewsummary" table:
+ | -1- | -2- |
+ | Marks | 2.00/3.00 |
+ | Grade | 66.67 out of 100.00 |
+ | Average CBM mark | 0.67 |
+ | Accuracy | 66.7% |
+ | CBM bonus | 0.0% |
+ | Accuracy + Bonus | 66.7% |
+ | C=3 | Responses: 1. Accuracy: 100%. (Optimal range 80% to 100%). You were OK using this certainty level. |
+ | C=2 | Responses: 1. Accuracy: 0%. (Optimal range 67% to 80%). You were a bit over-confident using this certainty level. |
+ | C=1 | Responses: 1. Accuracy: 100%. (Optimal range 0% to 67%). You were a bit under-confident using this certainty level. |
+ And I should see "CBM mark 1.00" in the "First question" "question"
+ And I should see "CBM mark -2.00" in the "Second question" "question"
+ And I should see "CBM mark 3.00" in the "Third question" "question"
+ # As teacher, confirm same quiz contents
+ And I am on the "Quiz 1" "quiz activity" page logged in as teacher1
+ And I press "Preview quiz"
+ And I should see "C=1 (Unsure: <67%)"
+ And I should see "C=2 (Mid: >67%)"
+ And I should see "C=3 (Quite sure: >80%)"
+ And I am on the "Quiz 1" "mod_quiz > Grades report" page
+ And I click on "Review attempt" "link" in the "Student One" "table_row"
+ # As teacher, confirm that the attempt result is same with student1 view
+ And the following should exist in the "quizreviewsummary" table:
+ | -1- | -2- |
+ | Marks | 2.00/3.00 |
+ | Grade | 66.67 out of 100.00 |
+ | Average CBM mark | 0.67 |
+ | Accuracy | 66.7% |
+ | CBM bonus | 0.0% |
+ | Accuracy + Bonus | 66.7% |
+ | C=3 | Responses: 1. Accuracy: 100%. (Optimal range 80% to 100%). You were OK using this certainty level. |
+ | C=2 | Responses: 1. Accuracy: 0%. (Optimal range 67% to 80%). You were a bit over-confident using this certainty level. |
+ | C=1 | Responses: 1. Accuracy: 100%. (Optimal range 0% to 67%). You were a bit under-confident using this certainty level. |
+ And I should see "CBM mark 1.00" in the "First question" "question"
+ And I should see "CBM mark -2.00" in the "Second question" "question"
+ And I should see "CBM mark 3.00" in the "Third question" "question"
diff --git a/mod/scorm/aicc.php b/mod/scorm/aicc.php
index 9598e3f17a616..73317cabdb856 100644
--- a/mod/scorm/aicc.php
+++ b/mod/scorm/aicc.php
@@ -217,8 +217,28 @@
$datamodel['[comments]'] = 'cmi.comments';
$datarows = explode("\r\n", $aiccdata);
reset($datarows);
- foreach ($datarows as $datarow) {
- if (($equal = strpos($datarow, '=')) !== false) {
+ $multirowvalue = '';
+ $multirowelement = '';
+ foreach ($datarows as $did => $datarow) {
+ $equal = strpos($datarow, '=');
+ if ($equal === false || !empty($multirowelement)) {
+ if (empty($multirowelement)) {
+ if (isset($datamodel[strtolower(trim($datarow))])) {
+ $multirowelement = $datamodel[strtolower(trim($datarow))];
+ } else {
+ // An element was passed by the external AICC package is not one we care about.
+ continue;
+ }
+ }
+ $multirowvalue .= $datarow."\r\n";
+ if (isset($datarows[$did + 1]) && substr($datarows[$did + 1], 0, 1) != '[') {
+ // This is a multiline row, we haven't found the end yet.
+ continue;
+ }
+ $value = rawurlencode($multirowvalue);
+ $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, $attempt, $multirowelement, $value);
+ $multirowvalue = $multirowelement = '';
+ } else {
$element = strtolower(trim(substr($datarow, 0, $equal)));
$value = trim(substr($datarow, $equal + 1));
if (isset($datamodel[$element])) {
@@ -302,17 +322,6 @@
break;
}
}
- } else {
- if (isset($datamodel[strtolower(trim($datarow))])) {
- $element = $datamodel[strtolower(trim($datarow))];
- $value = '';
- while ((($datarow = current($datarows)) !== false) && (substr($datarow, 0, 1) != '[')) {
- $value .= $datarow."\r\n";
- next($datarows);
- }
- $value = rawurlencode($value);
- $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, $attempt, $element, $value);
- }
}
}
if (($mode == 'browse') && ($initlessonstatus == 'not attempted')) {
diff --git a/mod/workshop/assessment.php b/mod/workshop/assessment.php
index 7e71af903eda9..0a8d6a7884923 100644
--- a/mod/workshop/assessment.php
+++ b/mod/workshop/assessment.php
@@ -153,6 +153,7 @@
$feedbackform = $workshop->get_feedbackreviewer_form($PAGE->url, $assessment, $options);
if ($data = $feedbackform->get_data()) {
$workshop->evaluate_assessment($assessment, $data, $cansetassessmentweight, $canoverridegrades);
+ $workshop->aggregate_grading_grades();
redirect($workshop->view_url());
}
}
diff --git a/mod/workshop/tests/behat/workshop_grade.feature b/mod/workshop/tests/behat/workshop_grade.feature
new file mode 100644
index 0000000000000..57f67831a7098
--- /dev/null
+++ b/mod/workshop/tests/behat/workshop_grade.feature
@@ -0,0 +1,122 @@
+@mod @mod_workshop
+Feature: Workshop grade submission and assessment
+ In order to use workshop activity
+ As a teacher
+ I need to be able to grade student's submissions and feedbacks
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Sam1 | Student1 | student1@example.com |
+ | student2 | Sam2 | Student2 | student2@example.com |
+ | student3 | Sam3 | Student3 | student3@example.com |
+ | student4 | Sam4 | Student4 | student3@example.com |
+ | teacher1 | Terry1 | Teacher1 | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname |
+ | Course1 | c1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | c1 | student |
+ | student2 | c1 | student |
+ | student3 | c1 | student |
+ | student4 | c1 | student |
+ | teacher1 | c1 | editingteacher |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber | submissiontypetext | submissiontypefile | grade | gradinggrade | gradedecimals | overallfeedbackmethod |
+ | workshop | TestWorkshop | Test workshop description | c1 | workshop1 | 2 | 1 | 10 | 5 | 1 | 2 |
+
+ Scenario: Assess submissions and gradings in workshop with javascript enabled
+ # teacher1 sets up assessment form and changes the phase to submission
+ Given I log in as "teacher1"
+ And I am on the "Course1" course page logged in as teacher1
+ And I edit assessment form in workshop "TestWorkshop" as:
+ | id_description__idx_0_editor | Aspect1 |
+ | id_description__idx_1_editor | |
+ | id_description__idx_2_editor | |
+ And I change phase in workshop "TestWorkshop" to "Submission phase"
+ # student1 submits
+ And I am on the TestWorkshop "workshop activity" page logged in as student1
+ And I should see "Submit your work"
+ And I add a submission in workshop "TestWorkshop" as:
+ | Title | Submission1 |
+ | Submission content | Some content |
+ And I should see "Submission1"
+ # teacher1 allocates reviewers and changes the phase to assessment
+ When I am on the TestWorkshop "workshop activity" page logged in as teacher1
+ Then the following should exist in the "grading-report" table:
+ | First name / Last name | Submission / Last modified |
+ | Sam1 Student1 | Submission1 |
+ | Sam2 Student2 | No submission found for this user |
+ | Sam3 Student3 | No submission found for this user |
+ | Sam4 Student4 | No submission found for this user |
+ And I allocate submissions in workshop "TestWorkshop" as:
+ | Participant | Reviewer |
+ | Sam1 Student1 | Sam2 Student2 |
+ | Sam1 Student1 | Sam3 Student3 |
+ | Sam1 Student1 | Sam4 Student4 |
+ And I am on the TestWorkshop "workshop activity" page
+ And I should see "to allocate: 0"
+ And I change phase in workshop "TestWorkshop" to "Assessment phase"
+ # student2 assesses work of student1
+ And I am on the TestWorkshop "workshop activity" page logged in as student2
+ And I should see "Submission1"
+ And I should see "Sam1 Student1"
+ And I assess submission "Sam1" in workshop "TestWorkshop" as:
+ | grade__idx_0 | 10 / 10 |
+ | peercomment__idx_0 | Amazing |
+ | Feedback for the author | Good work |
+ And I should see "Already graded"
+ # student3 assesses work of student1
+ And I am on the TestWorkshop "workshop activity" page logged in as student3
+ And I should see "Submission1"
+ And I should see "Sam1 Student1"
+ And I assess submission "Sam1" in workshop "TestWorkshop" as:
+ | grade__idx_0 | 10 / 10 |
+ | peercomment__idx_0 | Amazing |
+ | Feedback for the author | Good work |
+ And I should see "Already graded"
+ # student4 assesses work of student1
+ And I am on the TestWorkshop "workshop activity" page logged in as student4
+ And I should see "Submission1"
+ And I should see "Sam1 Student1"
+ And I assess submission "Sam1" in workshop "TestWorkshop" as:
+ | grade__idx_0 | 6 / 10 |
+ | peercomment__idx_0 | You can do better |
+ | Feedback for the author | Good work |
+ And I should see "Already graded"
+ # teacher1 makes sure he can see all peer grades and changes to grading evaluation phase
+ And I am on the TestWorkshop "workshop activity" page logged in as teacher1
+ And I should see grade "10.0" for workshop participant "Sam1" set by peer "Sam2"
+ And I should see grade "10.0" for workshop participant "Sam1" set by peer "Sam3"
+ And I should see grade "6.0" for workshop participant "Sam1" set by peer "Sam4"
+ And the following should exist in the "grading-report" table:
+ | First name / Last name | Submission / Last modified |
+ | Sam2 Student2 | No submission found for this user |
+ | Sam3 Student3 | No submission found for this user |
+ | Sam4 Student4 | No submission found for this user |
+ And I change phase in workshop "TestWorkshop" to "Grading evaluation phase"
+ And I press "Re-calculate grades"
+ And the following should exist in the "grading-report" table:
+ | First name / Last name | Submission / Last modified | Grade for submission (of 10.0) | Grades given | Grade for assessment (of 5.0) |
+ | Sam1 Student1 | Submission1 | 8.7 | - | - |
+ | Sam2 Student2 | No submission found for this user | - | 10.0 (5.0) | 5.0 |
+ | Sam3 Student3 | No submission found for this user | - | 10.0 (5.0) | 5.0 |
+ | Sam4 Student4 | No submission found for this user | - | 6.0 (3.2) | 3.2 |
+ And I click on "6.0 (3.2)" "link" in the "Sam4 Student4" "table_row"
+ And I set the following fields to these values:
+ | Override grade for assessment | 4 |
+ And I press "Save and close"
+ And the following should exist in the "grading-report" table:
+ | First name / Last name | Grades given | Grade for assessment (of 5.0) |
+ | Sam1 Student1 | - | - |
+ | Sam4 Student4 | 6.0 (3.2 / 4.0) | 4.0 |
+ # Undo teacher1 overrides the grade on assessment by student2
+ And I click on "6.0 (3.2 / 4.0)" "link" in the "Sam4 Student4" "table_row"
+ And I set the following fields to these values:
+ | Override grade for assessment | Not overridden |
+ And I press "Save and close"
+ And the following should exist in the "grading-report" table:
+ | First name / Last name | Grades given | Grade for assessment (of 5.0) |
+ | Sam1 Student1 | - | - |
+ | Sam4 Student4 | 6.0 (3.2) | 3.2 |
diff --git a/my/lib.php b/my/lib.php
index 1ed7676ceed2c..b20b1652a06a4 100644
--- a/my/lib.php
+++ b/my/lib.php
@@ -109,9 +109,11 @@ function my_copy_page(
$blockinstances = $DB->get_records('block_instances', array('parentcontextid' => $systemcontext->id,
'pagetypepattern' => $pagetype,
'subpagepattern' => $systempage->id));
+ $roles = get_all_roles();
$newblockinstanceids = [];
foreach ($blockinstances as $instance) {
$originalid = $instance->id;
+ $originalcontext = context_block::instance($originalid);
unset($instance->id);
$instance->parentcontextid = $usercontext->id;
$instance->subpagepattern = $page->id;
@@ -126,6 +128,22 @@ function my_copy_page(
instance: $originalid to new block instance: $instance->id for
block: $instance->blockname", DEBUG_DEVELOPER);
}
+ // Check if there are any overrides on this block instance.
+ // We check against all roles, not just roles assigned to the user.
+ // This is so any overrides that are applied to the system default page
+ // will be applied to the user's page as well, even if their role assignment changes in the future.
+ foreach ($roles as $role) {
+ $rolecapabilities = get_capabilities_from_role_on_context($role, $originalcontext);
+ // If there are overrides, then apply them to the new block instance.
+ foreach ($rolecapabilities as $rolecapability) {
+ role_change_permission(
+ $rolecapability->roleid,
+ $blockcontext,
+ $rolecapability->capability,
+ $rolecapability->permission
+ );
+ }
+ }
}
// Clone block position overrides.
diff --git a/question/behaviour/rendererbase.php b/question/behaviour/rendererbase.php
index db5ff9ebbc6f7..7c06cb4c965c0 100644
--- a/question/behaviour/rendererbase.php
+++ b/question/behaviour/rendererbase.php
@@ -234,13 +234,14 @@ protected function submit_button(question_attempt $qa, question_display_options
'type' => 'submit',
'id' => $qa->get_behaviour_field_name('submit'),
'name' => $qa->get_behaviour_field_name('submit'),
- 'value' => get_string('check', 'question'),
+ 'value' => 1,
'class' => 'submit btn btn-secondary',
);
if ($options->readonly) {
$attributes['disabled'] = 'disabled';
}
- $output = html_writer::empty_tag('input', $attributes);
+ $output = html_writer::tag('button',
+ $options->add_question_identifier_to_label(get_string('check', 'question'), true), $attributes);
if (!$options->readonly) {
$this->page->requires->js_init_call('M.core_question_engine.init_submit_button',
array($attributes['id']));
diff --git a/question/classes/statistics/questions/all_calculated_for_qubaid_condition.php b/question/classes/statistics/questions/all_calculated_for_qubaid_condition.php
index fa3b6dc434891..f02898caa4e13 100644
--- a/question/classes/statistics/questions/all_calculated_for_qubaid_condition.php
+++ b/question/classes/statistics/questions/all_calculated_for_qubaid_condition.php
@@ -211,17 +211,21 @@ public function get_cached($qubaids) {
foreach ($questionstatrecs as $fromdb) {
if (is_null($fromdb->variant)) {
if ($fromdb->slot) {
+ if (!isset($this->questionstats[$fromdb->slot])) {
+ debugging('Statistics found for slot ' . $fromdb->slot .
+ ' in stats ' . json_encode($qubaids->from_where_params()) .
+ ' which is not an analysable question.', DEBUG_DEVELOPER);
+ }
$this->questionstats[$fromdb->slot]->populate_from_record($fromdb);
- // Array created in constructor and populated from question.
} else {
$this->subquestionstats[$fromdb->questionid] = new calculated_for_subquestion();
$this->subquestionstats[$fromdb->questionid]->populate_from_record($fromdb);
if (isset($this->subquestions[$fromdb->questionid])) {
$this->subquestionstats[$fromdb->questionid]->question =
- $this->subquestions[$fromdb->questionid];
+ $this->subquestions[$fromdb->questionid];
} else {
- $this->subquestionstats[$fromdb->questionid]->question =
- question_bank::get_qtype('missingtype', false)->make_deleted_instance($fromdb->questionid, 1);
+ $this->subquestionstats[$fromdb->questionid]->question = question_bank::get_qtype(
+ 'missingtype', false)->make_deleted_instance($fromdb->questionid, 1);
}
}
}
@@ -230,13 +234,24 @@ public function get_cached($qubaids) {
foreach ($questionstatrecs as $fromdb) {
if (!is_null($fromdb->variant)) {
if ($fromdb->slot) {
+ if (!isset($this->questionstats[$fromdb->slot])) {
+ debugging('Statistics found for slot ' . $fromdb->slot .
+ ' in stats ' . json_encode($qubaids->from_where_params()) .
+ ' which is not an analysable question.', DEBUG_DEVELOPER);
+ continue;
+ }
$newcalcinstance = new calculated();
$this->questionstats[$fromdb->slot]->variantstats[$fromdb->variant] = $newcalcinstance;
$newcalcinstance->question = $this->questionstats[$fromdb->slot]->question;
} else {
$newcalcinstance = new calculated_for_subquestion();
$this->subquestionstats[$fromdb->questionid]->variantstats[$fromdb->variant] = $newcalcinstance;
- $newcalcinstance->question = $this->subquestions[$fromdb->questionid];
+ if (isset($this->subquestions[$fromdb->questionid])) {
+ $newcalcinstance->question = $this->subquestions[$fromdb->questionid];
+ } else {
+ $newcalcinstance->question = question_bank::get_qtype(
+ 'missingtype', false)->make_deleted_instance($fromdb->questionid, 1);
+ }
}
$newcalcinstance->populate_from_record($fromdb);
}
diff --git a/question/engine/datalib.php b/question/engine/datalib.php
index 0c32fe44f628a..b4b73bb749b8a 100644
--- a/question/engine/datalib.php
+++ b/question/engine/datalib.php
@@ -560,7 +560,9 @@ public function load_questions_usages_by_activity($qubaids) {
* @return array of records. See the SQL in this function to see the fields available.
*/
public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots = null, $fields = null) {
- if ($slots !== null) {
+ if ($slots === []) {
+ return [];
+ } else if ($slots !== null) {
[$slottest, $params] = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
$slotwhere = " AND qa.slot {$slottest}";
} else {
diff --git a/question/engine/tests/datalib_reporting_queries_test.php b/question/engine/tests/datalib_reporting_queries_test.php
index 993e88663681d..bad8a26abb1a1 100644
--- a/question/engine/tests/datalib_reporting_queries_test.php
+++ b/question/engine/tests/datalib_reporting_queries_test.php
@@ -21,6 +21,8 @@
use question_engine;
use question_engine_data_mapper;
use question_state;
+use quiz;
+use quiz_attempt;
defined('MOODLE_INTERNAL') || die();
@@ -332,4 +334,49 @@ protected function dotest_question_attempt_latest_state_view() {
'state' => (string) question_state::$gaveup,
), $state);
}
+
+ /**
+ * Test that a Quiz with only description questions wont break \quiz_statistics\task\recalculate.
+ *
+ * @covers \quiz_statistics\task\recalculate::execute
+ */
+ public function test_quiz_with_description_questions_recalculate_statistics(): void {
+ $this->resetAfterTest();
+
+ // Create course with quiz module.
+ $course = $this->getDataGenerator()->create_course();
+ $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+ $layout = '1';
+ $quiz = $quizgenerator->create_instance([
+ 'course' => $course->id,
+ 'grade' => 0.0, 'sumgrades' => 1,
+ 'layout' => $layout
+ ]);
+
+ // Add question of type description to quiz.
+ $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+ $cat = $questiongenerator->create_question_category();
+ $question = $questiongenerator->create_question('description', null, ['category' => $cat->id]);
+ quiz_add_quiz_question($question->id, $quiz);
+
+ // Create attempt.
+ $user = $this->getDataGenerator()->create_user();
+ $quizobj = quiz::create($quiz->id, $user->id);
+ $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+ $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+ $timenow = time();
+ $attempt = quiz_create_attempt($quizobj, 1, null, $timenow, false, $user->id);
+ quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+ quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+ // Submit attempt.
+ $attemptobj = quiz_attempt::create($attempt->id);
+ $attemptobj->process_submitted_actions($timenow, false);
+ $attemptobj->process_finish($timenow, false);
+
+ // Calculate the statistics.
+ $this->expectOutputRegex('~.*Calculations completed.*~');
+ $statisticstask = new \quiz_statistics\task\recalculate();
+ $statisticstask->execute();
+ }
}
diff --git a/question/engine/tests/helpers.php b/question/engine/tests/helpers.php
index c7da45ae5841d..3c1a754357683 100644
--- a/question/engine/tests/helpers.php
+++ b/question/engine/tests/helpers.php
@@ -1229,7 +1229,7 @@ protected function get_contains_button_expectation($name, $value = null, $enable
} else if ($enabled === false) {
$expectedattributes['disabled'] = 'disabled';
}
- return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes);
+ return new question_contains_tag_with_attributes('button', $expectedattributes, $forbiddenattributes);
}
/**
diff --git a/question/format.php b/question/format.php
index 620fc55f41182..db8ffec063150 100644
--- a/question/format.php
+++ b/question/format.php
@@ -474,9 +474,6 @@ public function importprocess() {
$questionversion->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
$questionversion->id = $DB->insert_record('question_versions', $questionversion);
- $event = \core\event\question_created::create_from_question_instance($question, $this->importcontext);
- $event->trigger();
-
if (isset($question->questiontextitemid)) {
$question->questiontext = file_save_draft_area_files($question->questiontextitemid,
$this->importcontext->id, 'question', 'questiontext', $question->id,
@@ -504,6 +501,8 @@ public function importprocess() {
// Now to save all the answers and type-specific options
$result = question_bank::get_qtype($question->qtype)->save_question_options($question);
+ $event = \core\event\question_created::create_from_question_instance($question, $this->importcontext);
+ $event->trigger();
if (core_tag_tag::is_enabled('core_question', 'question')) {
// Is the current context we're importing in a course context?
diff --git a/question/format/aiken/format.php b/question/format/aiken/format.php
index ab76310b15735..951923368bc2b 100644
--- a/question/format/aiken/format.php
+++ b/question/format/aiken/format.php
@@ -61,6 +61,10 @@ public function provide_export() {
return true;
}
+ public function validate_file(stored_file $file): string {
+ return $this->validate_is_utf8_file($file);
+ }
+
public function readquestions($lines) {
$questions = array();
$question = null;
diff --git a/question/type/essay/tests/behat/non_form_fields.feature b/question/type/essay/tests/behat/non_form_fields.feature
new file mode 100644
index 0000000000000..52bd95aeebbb3
--- /dev/null
+++ b/question/type/essay/tests/behat/non_form_fields.feature
@@ -0,0 +1,29 @@
+@editor @qtype @qtype_essay @javascript
+Feature: Set editor values when the editor is not in a form
+ As an automated tester
+ In order to use a non-form editor
+ I need to set values
+
+ Background:
+ Given the following "users" exist:
+ | username |
+ | teacher |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher | C1 | editingteacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | template |
+ | Test questions | essay | essay | editorfilepicker |
+
+ Scenario: Preview an Essay question that uses the HTML editor with embedded files.
+ When I am on the "essay" "core_question > preview" page logged in as teacher
+ And I expand all fieldsets
+ And I set the following fields to these values:
+ | Answer text Question 1 | The cat sat on the mat. Then it ate a frog. |
+ And I press "Submit and finish"
diff --git a/question/type/multianswer/amd/build/feedback.min.js b/question/type/multianswer/amd/build/feedback.min.js
new file mode 100644
index 0000000000000..21978a35ada55
--- /dev/null
+++ b/question/type/multianswer/amd/build/feedback.min.js
@@ -0,0 +1,10 @@
+define("qtype_multianswer/feedback",["exports","theme_boost/popover","jquery"],(function(_exports,_popover,_jquery){var obj;
+/**
+ * Backward compatibility file for the old popover.js
+ *
+ * @module qtype_multianswer/feedback
+ * @copyright 2023 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};const SELECTORS_FEEDBACK_TRIGGER='.feedbacktrigger[data-toggle="popover"]';let feedbackInitialised=!1;var _default={initPopovers:()=>{feedbackInitialised||((0,_jquery.default)(SELECTORS_FEEDBACK_TRIGGER).popover(),document.addEventListener("click",(e=>{e.target.closest(SELECTORS_FEEDBACK_TRIGGER)&&e.preventDefault()})),feedbackInitialised=!0)}};return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=feedback.min.js.map
\ No newline at end of file
diff --git a/question/type/multianswer/amd/build/feedback.min.js.map b/question/type/multianswer/amd/build/feedback.min.js.map
new file mode 100644
index 0000000000000..21a011d518dbb
--- /dev/null
+++ b/question/type/multianswer/amd/build/feedback.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"feedback.min.js","sources":["../src/feedback.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Backward compatibility file for the old popover.js\n *\n * @module qtype_multianswer/feedback\n * @copyright 2023 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport 'theme_boost/popover';\nimport $ from 'jquery';\n\n/** @type {object} Contains the list of selectors for this module. */\nconst SELECTORS = {\n FEEDBACK_TRIGGER: '.feedbacktrigger[data-toggle=\"popover\"]',\n};\n\n/** @type {boolean} Flag to indicate whether the feedback popovers have been already initialised. */\nlet feedbackInitialised = false;\n\n/**\n * Function to initialise the feedback popovers.\n */\nconst initPopovers = () => {\n if (!feedbackInitialised) {\n $(SELECTORS.FEEDBACK_TRIGGER).popover();\n\n document.addEventListener('click', (e) => {\n if (e.target.closest(SELECTORS.FEEDBACK_TRIGGER)) {\n e.preventDefault();\n }\n });\n feedbackInitialised = true;\n }\n};\n\nexport default {\n initPopovers: initPopovers,\n};\n"],"names":["SELECTORS","feedbackInitialised","initPopovers","popover","document","addEventListener","e","target","closest","preventDefault"],"mappings":";;;;;;;mJA2BMA,2BACgB,8CAIlBC,qBAAsB,eAkBX,CACXC,aAdiB,KACZD,0CACCD,4BAA4BG,UAE9BC,SAASC,iBAAiB,SAAUC,IAC5BA,EAAEC,OAAOC,QAAQR,6BACjBM,EAAEG,oBAGVR,qBAAsB"}
\ No newline at end of file
diff --git a/question/type/multianswer/amd/src/feedback.js b/question/type/multianswer/amd/src/feedback.js
new file mode 100644
index 0000000000000..9e1eb8fbb256e
--- /dev/null
+++ b/question/type/multianswer/amd/src/feedback.js
@@ -0,0 +1,53 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Backward compatibility file for the old popover.js
+ *
+ * @module qtype_multianswer/feedback
+ * @copyright 2023 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import 'theme_boost/popover';
+import $ from 'jquery';
+
+/** @type {object} Contains the list of selectors for this module. */
+const SELECTORS = {
+ FEEDBACK_TRIGGER: '.feedbacktrigger[data-toggle="popover"]',
+};
+
+/** @type {boolean} Flag to indicate whether the feedback popovers have been already initialised. */
+let feedbackInitialised = false;
+
+/**
+ * Function to initialise the feedback popovers.
+ */
+const initPopovers = () => {
+ if (!feedbackInitialised) {
+ $(SELECTORS.FEEDBACK_TRIGGER).popover();
+
+ document.addEventListener('click', (e) => {
+ if (e.target.closest(SELECTORS.FEEDBACK_TRIGGER)) {
+ e.preventDefault();
+ }
+ });
+ feedbackInitialised = true;
+ }
+};
+
+export default {
+ initPopovers: initPopovers,
+};
diff --git a/question/type/multianswer/question.php b/question/type/multianswer/question.php
index 62036c263a47b..a52eec83b05a0 100644
--- a/question/type/multianswer/question.php
+++ b/question/type/multianswer/question.php
@@ -147,6 +147,9 @@ public function get_min_fraction() {
$fractionmax += $subq->defaultmark;
$fractionsum += $subq->defaultmark * $subq->get_min_fraction();
}
+ if (empty($fractionsum)) {
+ return 0;
+ }
return $fractionsum / (!empty($this->subquestions) ? $fractionmax : 1);
}
@@ -157,6 +160,9 @@ public function get_max_fraction() {
$fractionmax += $subq->defaultmark;
$fractionsum += $subq->defaultmark * $subq->get_max_fraction();
}
+ if (empty($fractionsum)) {
+ return 1;
+ }
return $fractionsum / (!empty($this->subquestions) ? $fractionmax : 1);
}
@@ -299,6 +305,9 @@ public function grade_response(array $response) {
$overallstate = $this->combine_states($overallstate, $newstate);
}
}
+ if (empty($fractionmax)) {
+ return array(null, $overallstate ?? question_state::$finished);
+ }
return array($fractionsum / $fractionmax, $overallstate);
}
diff --git a/question/type/multianswer/renderer.php b/question/type/multianswer/renderer.php
index 528e7aacda47d..662ee3d45a6c6 100644
--- a/question/type/multianswer/renderer.php
+++ b/question/type/multianswer/renderer.php
@@ -208,19 +208,22 @@ protected function feedback_popup(question_graded_automatically $subq,
* @return string
*/
protected function get_feedback_image(string $icon, string $feedbackcontents): string {
+ global $PAGE;
if ($icon === '') {
return '';
}
+ $PAGE->requires->js_call_amd('qtype_multianswer/feedback', 'initPopovers');
+
return html_writer::link('#', $icon, [
'role' => 'button',
'tabindex' => 0,
- 'class' => 'btn btn-link p-0',
+ 'class' => 'feedbacktrigger btn btn-link p-0',
'data-toggle' => 'popover',
'data-container' => 'body',
'data-content' => $feedbackcontents,
'data-placement' => 'right',
- 'data-trigger' => 'focus',
+ 'data-trigger' => 'hover focus',
'data-html' => 'true',
]);
}
diff --git a/question/type/multianswer/tests/helper.php b/question/type/multianswer/tests/helper.php
index 9c6e2d2595ddd..6469b2a808d64 100644
--- a/question/type/multianswer/tests/helper.php
+++ b/question/type/multianswer/tests/helper.php
@@ -37,7 +37,7 @@
*/
class qtype_multianswer_test_helper extends question_test_helper {
public function get_test_questions() {
- return array('twosubq', 'fourmc', 'numericalzero', 'dollarsigns', 'multiple');
+ return array('twosubq', 'fourmc', 'numericalzero', 'dollarsigns', 'multiple', 'zeroweight');
}
/**
@@ -486,4 +486,48 @@ public function make_multianswer_question_multiple() {
return $q;
}
+ /**
+ * Makes a multianswer question with zero weight.
+ * This is used for testing the MDL-77378 bug.
+ * @return qtype_multianswer_question
+ */
+ public function make_multianswer_question_zeroweight() {
+ question_bank::load_question_definition_classes('multianswer');
+ $q = new qtype_multianswer_question();
+ test_question_maker::initialise_a_question($q);
+ $q->name = 'Zero weight';
+ $q->questiontext =
+ 'Optional question: {#1}.';
+ $q->generalfeedback = '';
+ $q->qtype = question_bank::get_qtype('multianswer');
+ $q->textfragments = array(
+ 'Optional question: ',
+ '.',
+ );
+ $q->places = array('1' => '1');
+
+ // Shortanswer subquestion.
+ question_bank::load_question_definition_classes('shortanswer');
+ $sa = new qtype_shortanswer_question();
+ test_question_maker::initialise_a_question($sa);
+ $sa->name = 'Zero weight';
+ $sa->questiontext = '{0:SHORTANSWER:~%0%Input box~%100%*}';
+ $sa->questiontextformat = FORMAT_HTML;
+ $sa->generalfeedback = '';
+ $sa->generalfeedbackformat = FORMAT_HTML;
+ $sa->usecase = true;
+ $sa->answers = array(
+ 13 => new question_answer(13, 'Input box', 0.0, '', FORMAT_HTML),
+ 14 => new question_answer(14, '*', 1.0, '', FORMAT_HTML),
+ );
+ $sa->qtype = question_bank::get_qtype('shortanswer');
+ $sa->defaultmark = 0;
+
+ $q->subquestions = array(
+ 1 => $sa,
+ );
+
+ return $q;
+ }
+
}
diff --git a/question/type/multianswer/tests/question_test.php b/question/type/multianswer/tests/question_test.php
index 407d8b97487ce..5d0beed7a8f0d 100644
--- a/question/type/multianswer/tests/question_test.php
+++ b/question/type/multianswer/tests/question_test.php
@@ -350,4 +350,23 @@ public function test_update_attempt_state_date_from_old_version_ok() {
$newquestion->update_attempt_state_data_for_new_version($oldstep, $question));
}
+ /**
+ * Test functions work with zero weight.
+ * This is used for testing the MDL-77378 bug.
+ */
+ public function test_zeroweight() {
+ $this->resetAfterTest();
+ /** @var \qtype_multianswer_question $question */
+ $question = \test_question_maker::make_question('multianswer', 'zeroweight');
+ $question->start_attempt(new question_attempt_step(), 1);
+
+ $this->assertEquals([null, question_state::$gradedright], $question->grade_response(
+ ['sub1_answer' => 'Something']));
+ $this->assertEquals([null, question_state::$gradedwrong], $question->grade_response(
+ ['sub1_answer' => 'Input box']));
+
+ $this->assertEquals(1, $question->get_max_fraction());
+ $this->assertEquals(0, $question->get_min_fraction());
+ }
+
}
diff --git a/report/insights/done.php b/report/insights/done.php
index e11aff98843e8..15257b91c15da 100644
--- a/report/insights/done.php
+++ b/report/insights/done.php
@@ -37,7 +37,7 @@
exit(0);
}
-$PAGE->set_title(get_site()->fullname);
+$PAGE->set_title(get_string('insights', 'report_insights'));
$PAGE->set_url(new \moodle_url('/report/insights/done.php'));
echo $OUTPUT->header();
diff --git a/report/log/index.php b/report/log/index.php
index 8db48419616a1..e3c7187efc2aa 100644
--- a/report/log/index.php
+++ b/report/log/index.php
@@ -143,7 +143,7 @@
if ($course->id == $SITE->id) {
admin_externalpage_setup('reportlog', '', null, '', array('pagelayout' => 'report'));
- $PAGE->set_title($SITE->shortname .': '. $strlogs);
+ $PAGE->set_title($strlogs);
$PAGE->set_primary_active_tab('siteadminnode');
} else {
$PAGE->set_title($course->shortname .': '. $strlogs);
diff --git a/report/progress/amd/build/completion_override.min.js b/report/progress/amd/build/completion_override.min.js
index 7cc1a4f6c8d67..b377ace109dcb 100644
--- a/report/progress/amd/build/completion_override.min.js
+++ b/report/progress/amd/build/completion_override.min.js
@@ -6,6 +6,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.1
*/
-define("report_progress/completion_override",["jquery","core/ajax","core/str","core/modal_factory","core/modal_events","core/notification","core/custom_interaction_events","core/templates"],(function($,Ajax,Str,ModalFactory,ModalEvents,Notification,CustomEvents,Templates){var userFullName,triggerElement,userConfirm=function(e,data){data.originalEvent.preventDefault(),data.originalEvent.stopPropagation(),e.preventDefault(),e.stopPropagation();var elemData=(triggerElement=$(e.currentTarget)).data("changecompl").split("-"),override={userid:elemData[0],cmid:elemData[1],newstate:elemData[2]},newStateStr=1==override.newstate?"completion-y":"completion-n";Str.get_strings([{key:newStateStr,component:"completion"}]).then((function(strings){return Str.get_strings([{key:"confirm",component:"moodle"},{key:"areyousureoverridecompletion",component:"completion",param:strings[0]}])})).then((function(strings){return ModalFactory.create({type:ModalFactory.types.SAVE_CANCEL,title:strings[0],body:strings[1]})})).then((function(modal){modal.getRoot().on(ModalEvents.save,(function(){!function(override){Templates.render("core/loading",{}).then((function(html){return triggerElement.append(html),Ajax.call([{methodname:"core_completion_override_activity_completion_status",args:override}])[0]})).then((function(results){var completionState=results.state>0?1:0,tooltipKey=completionState?"completion-y-override":"completion-n-override";Str.get_string(tooltipKey,"completion",userFullName).then((function(stateString){var params={state:stateString,date:"",user:triggerElement.attr("data-userfullname"),activity:triggerElement.attr("data-activityname")};return Str.get_string("progress-title","completion",params)})).then((function(titleString){var tracking,completionTracking=triggerElement.attr("data-completiontracking");return Templates.renderPix((tracking=completionTracking,completionState>0?"i/completion-"+tracking+"-y-override":"i/completion-"+tracking+"-n-override"),"core",titleString)})).then((function(html){var oppositeState=completionState>0?0:1;triggerElement.find(".loading-icon").remove(),triggerElement.data("changecompl",override.userid+"-"+override.cmid+"-"+oppositeState),triggerElement.attr("data-changecompl",override.userid+"-"+override.cmid+"-"+oppositeState),triggerElement.children("img").replaceWith(html)})).catch(Notification.exception)})).catch(Notification.exception)}(override)})),modal.getRoot().on(ModalEvents.hidden,(function(){triggerElement.focus(),modal.destroy()})),modal.show()})).catch(Notification.exception)};return{init:function(fullName){userFullName=fullName,$("#completion-progress a.changecompl").each((function(index,element){CustomEvents.define(element,[CustomEvents.events.activate])})),$("#completion-progress").on(CustomEvents.events.activate,"a.changecompl",(function(e,data){userConfirm(e,data)}))}}}));
+define("report_progress/completion_override",["jquery","core/ajax","core/str","core/modal_factory","core/modal_events","core/notification","core/custom_interaction_events","core/templates","core/pending"],(function($,Ajax,Str,ModalFactory,ModalEvents,Notification,CustomEvents,Templates,Pending){var userFullName,triggerElement,userConfirm=function(e,data){data.originalEvent.preventDefault(),data.originalEvent.stopPropagation(),e.preventDefault(),e.stopPropagation();var elemData=(triggerElement=$(e.currentTarget)).data("changecompl").split("-"),override={userid:elemData[0],cmid:elemData[1],newstate:elemData[2]},newStateStr=1==override.newstate?"completion-y":"completion-n";Str.get_strings([{key:newStateStr,component:"completion"}]).then((function(strings){return Str.get_strings([{key:"confirm",component:"moodle"},{key:"areyousureoverridecompletion",component:"completion",param:strings[0]}])})).then((function(strings){return ModalFactory.create({type:ModalFactory.types.SAVE_CANCEL,title:strings[0],body:strings[1]})})).then((function(modal){modal.getRoot().on(ModalEvents.save,(function(){!function(override){const pendingPromise=new Pending("report_progress/compeletion_override/setOverride");Templates.render("core/loading",{}).then((function(html){return triggerElement.append(html),Ajax.call([{methodname:"core_completion_override_activity_completion_status",args:override}])[0]})).then((function(results){var completionState=results.state>0?1:0,tooltipKey=completionState?"completion-y-override":"completion-n-override";Str.get_string(tooltipKey,"completion",userFullName).then((function(stateString){var params={state:stateString,date:"",user:triggerElement.attr("data-userfullname"),activity:triggerElement.attr("data-activityname")};return Str.get_string("progress-title","completion",params)})).then((function(titleString){var tracking,completionTracking=triggerElement.attr("data-completiontracking");return Templates.renderPix((tracking=completionTracking,completionState>0?"i/completion-"+tracking+"-y-override":"i/completion-"+tracking+"-n-override"),"core",titleString)})).then((function(html){var oppositeState=completionState>0?0:1;triggerElement.find(".loading-icon").remove(),triggerElement.data("changecompl",override.userid+"-"+override.cmid+"-"+oppositeState),triggerElement.attr("data-changecompl",override.userid+"-"+override.cmid+"-"+oppositeState),triggerElement.children("img").replaceWith(html)})).catch(Notification.exception)})).then((()=>{pendingPromise.resolve()})).catch(Notification.exception)}(override)})),modal.getRoot().on(ModalEvents.hidden,(function(){triggerElement.focus(),modal.destroy()})),modal.show()})).catch(Notification.exception)};return{init:function(fullName){userFullName=fullName,$("#completion-progress a.changecompl").each((function(index,element){CustomEvents.define(element,[CustomEvents.events.activate])})),$("#completion-progress").on(CustomEvents.events.activate,"a.changecompl",(function(e,data){userConfirm(e,data)}))}}}));
//# sourceMappingURL=completion_override.min.js.map
\ No newline at end of file
diff --git a/report/progress/amd/build/completion_override.min.js.map b/report/progress/amd/build/completion_override.min.js.map
index b53f70086a53b..363b86de0f188 100644
--- a/report/progress/amd/build/completion_override.min.js.map
+++ b/report/progress/amd/build/completion_override.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"completion_override.min.js","sources":["../src/completion_override.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * AMD module to handle overriding activity completion status.\n *\n * @module report_progress/completion_override\n * @copyright 2016 onwards Eiz Eddin Al Katrib \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['jquery', 'core/ajax', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/notification',\n 'core/custom_interaction_events', 'core/templates'],\n function($, Ajax, Str, ModalFactory, ModalEvents, Notification, CustomEvents, Templates) {\n\n /**\n * @var {String} the full name of the current user.\n * @private\n */\n var userFullName;\n\n /**\n * @var {JQuery} JQuery object containing the element (completion link) that was most recently activated.\n * @private\n */\n var triggerElement;\n\n /**\n * Helper function to get the pix icon key based on the completion state.\n * @method getIconDescriptorFromState\n * @param {number} state The current completion state.\n * @param {string} tracking The completion tracking type, either 'manual' or 'auto'.\n * @return {string} the key for the respective icon.\n * @private\n */\n var getIconKeyFromState = function(state, tracking) {\n return state > 0 ? 'i/completion-' + tracking + '-y-override' : 'i/completion-' + tracking + '-n-override';\n };\n\n /**\n * Handles the confirmation of an override change, calling the web service to update it.\n * @method setOverride\n * @param {Object} override the override data\n * @private\n */\n var setOverride = function(override) {\n // Generate a loading spinner while we're working.\n Templates.render('core/loading', {}).then(function(html) {\n // Append the loading spinner to the trigger element.\n triggerElement.append(html);\n\n // Update the completion status override.\n return Ajax.call([{\n methodname: 'core_completion_override_activity_completion_status',\n args: override\n }])[0];\n }).then(function(results) {\n var completionState = (results.state > 0) ? 1 : 0;\n\n // Now, build the new title string, get the new icon, and update the DOM.\n var tooltipKey = completionState ? 'completion-y-override' : 'completion-n-override';\n Str.get_string(tooltipKey, 'completion', userFullName).then(function(stateString) {\n var params = {\n state: stateString,\n date: '',\n user: triggerElement.attr('data-userfullname'),\n activity: triggerElement.attr('data-activityname')\n };\n return Str.get_string('progress-title', 'completion', params);\n }).then(function(titleString) {\n var completionTracking = triggerElement.attr('data-completiontracking');\n return Templates.renderPix(getIconKeyFromState(completionState, completionTracking), 'core', titleString);\n }).then(function(html) {\n var oppositeState = completionState > 0 ? 0 : 1;\n triggerElement.find('.loading-icon').remove();\n triggerElement.data('changecompl', override.userid + '-' + override.cmid + '-' + oppositeState);\n triggerElement.attr('data-changecompl', override.userid + '-' + override.cmid + '-' + oppositeState);\n triggerElement.children(\"img\").replaceWith(html);\n return;\n }).catch(Notification.exception);\n\n return;\n }).catch(Notification.exception);\n };\n\n /**\n * Handler for activation of a completion status button element.\n * @method userConfirm\n * @param {Event} e the CustomEvents event (CustomEvents.events.activate in this case)\n * @param {Object} data an object containing the original event (click, keydown, etc.).\n * @private\n */\n var userConfirm = function(e, data) {\n data.originalEvent.preventDefault();\n data.originalEvent.stopPropagation();\n e.preventDefault();\n e.stopPropagation();\n\n triggerElement = $(e.currentTarget);\n var elemData = triggerElement.data('changecompl').split('-');\n var override = {\n userid: elemData[0],\n cmid: elemData[1],\n newstate: elemData[2]\n };\n var newStateStr = (override.newstate == 1) ? 'completion-y' : 'completion-n';\n\n Str.get_strings([\n {key: newStateStr, component: 'completion'}\n ]).then(function(strings) {\n return Str.get_strings([\n {key: 'confirm', component: 'moodle'},\n {key: 'areyousureoverridecompletion', component: 'completion', param: strings[0]}\n ]);\n }).then(function(strings) {\n // Create a save/cancel modal.\n return ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: strings[0],\n body: strings[1],\n });\n }).then(function(modal) {\n // Now set up the handlers for the confirmation or cancellation of the modal, and show it.\n\n // Confirmation only.\n modal.getRoot().on(ModalEvents.save, function() {\n setOverride(override);\n });\n\n // Confirming, closing, or cancelling will destroy the modal and return focus to the trigger element.\n modal.getRoot().on(ModalEvents.hidden, function() {\n triggerElement.focus();\n modal.destroy();\n });\n\n // Display.\n modal.show();\n return;\n }).catch(Notification.exception);\n };\n\n /**\n * Init this module which allows activity completion state to be changed via ajax.\n * @method init\n * @param {string} fullName The current user's full name.\n * @private\n */\n var init = function(fullName) {\n userFullName = fullName;\n\n // Register the click, space and enter events as activators for the trigger element.\n $('#completion-progress a.changecompl').each(function(index, element) {\n CustomEvents.define(element, [CustomEvents.events.activate]);\n });\n\n // Set the handler on the parent element (the table), but filter so the callback is only called for type children\n // having the '.changecompl' class. The element can then be accessed in the callback via e.currentTarget.\n $('#completion-progress').on(CustomEvents.events.activate, \"a.changecompl\", function(e, data) {\n userConfirm(e, data);\n });\n };\n\n return /** @alias module:report_progress/completion_override */ {\n init: init\n };\n });\n"],"names":["define","$","Ajax","Str","ModalFactory","ModalEvents","Notification","CustomEvents","Templates","userFullName","triggerElement","userConfirm","e","data","originalEvent","preventDefault","stopPropagation","elemData","currentTarget","split","override","userid","cmid","newstate","newStateStr","get_strings","key","component","then","strings","param","create","type","types","SAVE_CANCEL","title","body","modal","getRoot","on","save","render","html","append","call","methodname","args","results","completionState","state","tooltipKey","get_string","stateString","params","date","user","attr","activity","titleString","tracking","completionTracking","renderPix","oppositeState","find","remove","children","replaceWith","catch","exception","setOverride","hidden","focus","destroy","show","init","fullName","each","index","element","events","activate"],"mappings":";;;;;;;;AAuBAA,6CAAO,CAAC,SAAU,YAAa,WAAY,qBAAsB,oBAAqB,oBAC9E,iCAAkC,mBACtC,SAASC,EAAGC,KAAMC,IAAKC,aAAcC,YAAaC,aAAcC,aAAcC,eAMtEC,aAMAC,eAmEAC,YAAc,SAASC,EAAGC,MAC1BA,KAAKC,cAAcC,iBACnBF,KAAKC,cAAcE,kBACnBJ,EAAEG,iBACFH,EAAEI,sBAGEC,UADJP,eAAiBT,EAAEW,EAAEM,gBACSL,KAAK,eAAeM,MAAM,KACpDC,SAAW,CACXC,OAAQJ,SAAS,GACjBK,KAAML,SAAS,GACfM,SAAUN,SAAS,IAEnBO,YAAoC,GAArBJ,SAASG,SAAiB,eAAiB,eAE9DpB,IAAIsB,YAAY,CACZ,CAACC,IAAKF,YAAaG,UAAW,gBAC/BC,MAAK,SAASC,gBACN1B,IAAIsB,YAAY,CACnB,CAACC,IAAK,UAAWC,UAAW,UAC5B,CAACD,IAAK,+BAAgCC,UAAW,aAAcG,MAAOD,QAAQ,SAEnFD,MAAK,SAASC,gBAENzB,aAAa2B,OAAO,CACvBC,KAAM5B,aAAa6B,MAAMC,YACzBC,MAAON,QAAQ,GACfO,KAAMP,QAAQ,QAEnBD,MAAK,SAASS,OAIbA,MAAMC,UAAUC,GAAGlC,YAAYmC,MAAM,YAhF3B,SAASpB,UAEvBZ,UAAUiC,OAAO,eAAgB,IAAIb,MAAK,SAASc,aAE/ChC,eAAeiC,OAAOD,MAGfxC,KAAK0C,KAAK,CAAC,CACdC,WAAY,sDACZC,KAAM1B,YACN,MACLQ,MAAK,SAASmB,aACTC,gBAAmBD,QAAQE,MAAQ,EAAK,EAAI,EAG5CC,WAAaF,gBAAkB,wBAA0B,wBAC7D7C,IAAIgD,WAAWD,WAAY,aAAczC,cAAcmB,MAAK,SAASwB,iBAC7DC,OAAS,CACTJ,MAAOG,YACPE,KAAM,GACNC,KAAM7C,eAAe8C,KAAK,qBAC1BC,SAAU/C,eAAe8C,KAAK,6BAE3BrD,IAAIgD,WAAW,iBAAkB,aAAcE,WACvDzB,MAAK,SAAS8B,iBAlCiBC,SAmC1BC,mBAAqBlD,eAAe8C,KAAK,kCACtChD,UAAUqD,WApCaF,SAoCkCC,mBAAjBZ,gBAnCxC,EAAI,gBAAkBW,SAAW,cAAgB,gBAAkBA,SAAW,eAmCA,OAAQD,gBAC9F9B,MAAK,SAASc,UACToB,cAAgBd,gBAAkB,EAAI,EAAI,EAC9CtC,eAAeqD,KAAK,iBAAiBC,SACrCtD,eAAeG,KAAK,cAAeO,SAASC,OAAS,IAAMD,SAASE,KAAO,IAAMwC,eACjFpD,eAAe8C,KAAK,mBAAoBpC,SAASC,OAAS,IAAMD,SAASE,KAAO,IAAMwC,eACtFpD,eAAeuD,SAAS,OAAOC,YAAYxB,SAE5CyB,MAAM7D,aAAa8D,cAGvBD,MAAM7D,aAAa8D,WA4CdC,CAAYjD,aAIhBiB,MAAMC,UAAUC,GAAGlC,YAAYiE,QAAQ,WACnC5D,eAAe6D,QACflC,MAAMmC,aAIVnC,MAAMoC,UAEPN,MAAM7D,aAAa8D,kBAwBsC,CAC5DM,KAhBO,SAASC,UAChBlE,aAAekE,SAGf1E,EAAE,sCAAsC2E,MAAK,SAASC,MAAOC,SACzDvE,aAAaP,OAAO8E,QAAS,CAACvE,aAAawE,OAAOC,cAKtD/E,EAAE,wBAAwBsC,GAAGhC,aAAawE,OAAOC,SAAU,iBAAiB,SAASpE,EAAGC,MACpFF,YAAYC,EAAGC"}
\ No newline at end of file
+{"version":3,"file":"completion_override.min.js","sources":["../src/completion_override.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * AMD module to handle overriding activity completion status.\n *\n * @module report_progress/completion_override\n * @copyright 2016 onwards Eiz Eddin Al Katrib \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['jquery', 'core/ajax', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/notification',\n 'core/custom_interaction_events', 'core/templates', 'core/pending'],\n function($, Ajax, Str, ModalFactory, ModalEvents, Notification, CustomEvents, Templates, Pending) {\n\n /**\n * @var {String} the full name of the current user.\n * @private\n */\n var userFullName;\n\n /**\n * @var {JQuery} JQuery object containing the element (completion link) that was most recently activated.\n * @private\n */\n var triggerElement;\n\n /**\n * Helper function to get the pix icon key based on the completion state.\n * @method getIconDescriptorFromState\n * @param {number} state The current completion state.\n * @param {string} tracking The completion tracking type, either 'manual' or 'auto'.\n * @return {string} the key for the respective icon.\n * @private\n */\n var getIconKeyFromState = function(state, tracking) {\n return state > 0 ? 'i/completion-' + tracking + '-y-override' : 'i/completion-' + tracking + '-n-override';\n };\n\n /**\n * Handles the confirmation of an override change, calling the web service to update it.\n * @method setOverride\n * @param {Object} override the override data\n * @private\n */\n var setOverride = function(override) {\n const pendingPromise = new Pending('report_progress/compeletion_override/setOverride');\n // Generate a loading spinner while we're working.\n Templates.render('core/loading', {}).then(function(html) {\n // Append the loading spinner to the trigger element.\n triggerElement.append(html);\n\n // Update the completion status override.\n return Ajax.call([{\n methodname: 'core_completion_override_activity_completion_status',\n args: override\n }])[0];\n }).then(function(results) {\n var completionState = (results.state > 0) ? 1 : 0;\n\n // Now, build the new title string, get the new icon, and update the DOM.\n var tooltipKey = completionState ? 'completion-y-override' : 'completion-n-override';\n Str.get_string(tooltipKey, 'completion', userFullName).then(function(stateString) {\n var params = {\n state: stateString,\n date: '',\n user: triggerElement.attr('data-userfullname'),\n activity: triggerElement.attr('data-activityname')\n };\n return Str.get_string('progress-title', 'completion', params);\n }).then(function(titleString) {\n var completionTracking = triggerElement.attr('data-completiontracking');\n return Templates.renderPix(getIconKeyFromState(completionState, completionTracking), 'core', titleString);\n }).then(function(html) {\n var oppositeState = completionState > 0 ? 0 : 1;\n triggerElement.find('.loading-icon').remove();\n triggerElement.data('changecompl', override.userid + '-' + override.cmid + '-' + oppositeState);\n triggerElement.attr('data-changecompl', override.userid + '-' + override.cmid + '-' + oppositeState);\n triggerElement.children(\"img\").replaceWith(html);\n return;\n }).catch(Notification.exception);\n\n return;\n })\n .then(() => {\n pendingPromise.resolve();\n return;\n }).catch(Notification.exception);\n };\n\n /**\n * Handler for activation of a completion status button element.\n * @method userConfirm\n * @param {Event} e the CustomEvents event (CustomEvents.events.activate in this case)\n * @param {Object} data an object containing the original event (click, keydown, etc.).\n * @private\n */\n var userConfirm = function(e, data) {\n data.originalEvent.preventDefault();\n data.originalEvent.stopPropagation();\n e.preventDefault();\n e.stopPropagation();\n\n triggerElement = $(e.currentTarget);\n var elemData = triggerElement.data('changecompl').split('-');\n var override = {\n userid: elemData[0],\n cmid: elemData[1],\n newstate: elemData[2]\n };\n var newStateStr = (override.newstate == 1) ? 'completion-y' : 'completion-n';\n\n Str.get_strings([\n {key: newStateStr, component: 'completion'}\n ]).then(function(strings) {\n return Str.get_strings([\n {key: 'confirm', component: 'moodle'},\n {key: 'areyousureoverridecompletion', component: 'completion', param: strings[0]}\n ]);\n }).then(function(strings) {\n // Create a save/cancel modal.\n return ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: strings[0],\n body: strings[1],\n });\n }).then(function(modal) {\n // Now set up the handlers for the confirmation or cancellation of the modal, and show it.\n\n // Confirmation only.\n modal.getRoot().on(ModalEvents.save, function() {\n setOverride(override);\n });\n\n // Confirming, closing, or cancelling will destroy the modal and return focus to the trigger element.\n modal.getRoot().on(ModalEvents.hidden, function() {\n triggerElement.focus();\n modal.destroy();\n });\n\n // Display.\n modal.show();\n return;\n }).catch(Notification.exception);\n };\n\n /**\n * Init this module which allows activity completion state to be changed via ajax.\n * @method init\n * @param {string} fullName The current user's full name.\n * @private\n */\n var init = function(fullName) {\n userFullName = fullName;\n\n // Register the click, space and enter events as activators for the trigger element.\n $('#completion-progress a.changecompl').each(function(index, element) {\n CustomEvents.define(element, [CustomEvents.events.activate]);\n });\n\n // Set the handler on the parent element (the table), but filter so the callback is only called for type children\n // having the '.changecompl' class. The element can then be accessed in the callback via e.currentTarget.\n $('#completion-progress').on(CustomEvents.events.activate, \"a.changecompl\", function(e, data) {\n userConfirm(e, data);\n });\n };\n\n return /** @alias module:report_progress/completion_override */ {\n init: init\n };\n });\n"],"names":["define","$","Ajax","Str","ModalFactory","ModalEvents","Notification","CustomEvents","Templates","Pending","userFullName","triggerElement","userConfirm","e","data","originalEvent","preventDefault","stopPropagation","elemData","currentTarget","split","override","userid","cmid","newstate","newStateStr","get_strings","key","component","then","strings","param","create","type","types","SAVE_CANCEL","title","body","modal","getRoot","on","save","pendingPromise","render","html","append","call","methodname","args","results","completionState","state","tooltipKey","get_string","stateString","params","date","user","attr","activity","titleString","tracking","completionTracking","renderPix","oppositeState","find","remove","children","replaceWith","catch","exception","resolve","setOverride","hidden","focus","destroy","show","init","fullName","each","index","element","events","activate"],"mappings":";;;;;;;;AAuBAA,6CAAO,CAAC,SAAU,YAAa,WAAY,qBAAsB,oBAAqB,oBAC9E,iCAAkC,iBAAkB,iBACxD,SAASC,EAAGC,KAAMC,IAAKC,aAAcC,YAAaC,aAAcC,aAAcC,UAAWC,aAMjFC,aAMAC,eAwEAC,YAAc,SAASC,EAAGC,MAC1BA,KAAKC,cAAcC,iBACnBF,KAAKC,cAAcE,kBACnBJ,EAAEG,iBACFH,EAAEI,sBAGEC,UADJP,eAAiBV,EAAEY,EAAEM,gBACSL,KAAK,eAAeM,MAAM,KACpDC,SAAW,CACXC,OAAQJ,SAAS,GACjBK,KAAML,SAAS,GACfM,SAAUN,SAAS,IAEnBO,YAAoC,GAArBJ,SAASG,SAAiB,eAAiB,eAE9DrB,IAAIuB,YAAY,CACZ,CAACC,IAAKF,YAAaG,UAAW,gBAC/BC,MAAK,SAASC,gBACN3B,IAAIuB,YAAY,CACnB,CAACC,IAAK,UAAWC,UAAW,UAC5B,CAACD,IAAK,+BAAgCC,UAAW,aAAcG,MAAOD,QAAQ,SAEnFD,MAAK,SAASC,gBAEN1B,aAAa4B,OAAO,CACvBC,KAAM7B,aAAa8B,MAAMC,YACzBC,MAAON,QAAQ,GACfO,KAAMP,QAAQ,QAEnBD,MAAK,SAASS,OAIbA,MAAMC,UAAUC,GAAGnC,YAAYoC,MAAM,YArF3B,SAASpB,gBACjBqB,eAAiB,IAAIjC,QAAQ,oDAEnCD,UAAUmC,OAAO,eAAgB,IAAId,MAAK,SAASe,aAE/CjC,eAAekC,OAAOD,MAGf1C,KAAK4C,KAAK,CAAC,CACdC,WAAY,sDACZC,KAAM3B,YACN,MACLQ,MAAK,SAASoB,aACTC,gBAAmBD,QAAQE,MAAQ,EAAK,EAAI,EAG5CC,WAAaF,gBAAkB,wBAA0B,wBAC7D/C,IAAIkD,WAAWD,WAAY,aAAc1C,cAAcmB,MAAK,SAASyB,iBAC7DC,OAAS,CACTJ,MAAOG,YACPE,KAAM,GACNC,KAAM9C,eAAe+C,KAAK,qBAC1BC,SAAUhD,eAAe+C,KAAK,6BAE3BvD,IAAIkD,WAAW,iBAAkB,aAAcE,WACvD1B,MAAK,SAAS+B,iBAnCiBC,SAoC1BC,mBAAqBnD,eAAe+C,KAAK,kCACtClD,UAAUuD,WArCaF,SAqCkCC,mBAAjBZ,gBApCxC,EAAI,gBAAkBW,SAAW,cAAgB,gBAAkBA,SAAW,eAoCA,OAAQD,gBAC9F/B,MAAK,SAASe,UACToB,cAAgBd,gBAAkB,EAAI,EAAI,EAC9CvC,eAAesD,KAAK,iBAAiBC,SACrCvD,eAAeG,KAAK,cAAeO,SAASC,OAAS,IAAMD,SAASE,KAAO,IAAMyC,eACjFrD,eAAe+C,KAAK,mBAAoBrC,SAASC,OAAS,IAAMD,SAASE,KAAO,IAAMyC,eACtFrD,eAAewD,SAAS,OAAOC,YAAYxB,SAE5CyB,MAAM/D,aAAagE,cAIzBzC,MAAK,KACFa,eAAe6B,aAEhBF,MAAM/D,aAAagE,WA4CdE,CAAYnD,aAIhBiB,MAAMC,UAAUC,GAAGnC,YAAYoE,QAAQ,WACnC9D,eAAe+D,QACfpC,MAAMqC,aAIVrC,MAAMsC,UAEPP,MAAM/D,aAAagE,kBAwBsC,CAC5DO,KAhBO,SAASC,UAChBpE,aAAeoE,SAGf7E,EAAE,sCAAsC8E,MAAK,SAASC,MAAOC,SACzD1E,aAAaP,OAAOiF,QAAS,CAAC1E,aAAa2E,OAAOC,cAKtDlF,EAAE,wBAAwBuC,GAAGjC,aAAa2E,OAAOC,SAAU,iBAAiB,SAAStE,EAAGC,MACpFF,YAAYC,EAAGC"}
\ No newline at end of file
diff --git a/report/progress/amd/src/completion_override.js b/report/progress/amd/src/completion_override.js
index fb0a4161f268a..40855418bfde3 100644
--- a/report/progress/amd/src/completion_override.js
+++ b/report/progress/amd/src/completion_override.js
@@ -22,8 +22,8 @@
* @since 3.1
*/
define(['jquery', 'core/ajax', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/notification',
- 'core/custom_interaction_events', 'core/templates'],
- function($, Ajax, Str, ModalFactory, ModalEvents, Notification, CustomEvents, Templates) {
+ 'core/custom_interaction_events', 'core/templates', 'core/pending'],
+ function($, Ajax, Str, ModalFactory, ModalEvents, Notification, CustomEvents, Templates, Pending) {
/**
* @var {String} the full name of the current user.
@@ -56,6 +56,7 @@ define(['jquery', 'core/ajax', 'core/str', 'core/modal_factory', 'core/modal_eve
* @private
*/
var setOverride = function(override) {
+ const pendingPromise = new Pending('report_progress/compeletion_override/setOverride');
// Generate a loading spinner while we're working.
Templates.render('core/loading', {}).then(function(html) {
// Append the loading spinner to the trigger element.
@@ -91,6 +92,10 @@ define(['jquery', 'core/ajax', 'core/str', 'core/modal_factory', 'core/modal_eve
return;
}).catch(Notification.exception);
+ return;
+ })
+ .then(() => {
+ pendingPromise.resolve();
return;
}).catch(Notification.exception);
};
diff --git a/repository/upload/lang/en/repository_upload.php b/repository/upload/lang/en/repository_upload.php
index 00d3cfa28303d..c93b7c989254d 100644
--- a/repository/upload/lang/en/repository_upload.php
+++ b/repository/upload/lang/en/repository_upload.php
@@ -27,7 +27,7 @@
$string['pluginname_help'] = 'Upload a file to Moodle';
$string['pluginname'] = 'Upload a file';
$string['upload:view'] = 'Use uploading in file picker';
-$string['upload_error_ini_size'] = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
+$string['upload_error_ini_size'] = 'The file is larger than the maximum size allowed.';
$string['upload_error_form_size'] = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
$string['upload_error_partial'] = 'The uploaded file was only partially uploaded.';
$string['upload_error_no_file'] = 'No file was uploaded.';
diff --git a/security.txt b/security.txt
index e420f6f2754a7..6e3d7d36cc8a9 100644
--- a/security.txt
+++ b/security.txt
@@ -1,6 +1,18 @@
+# Moodle HQ security submission form
Contact: https://moodle.org/security/report/
+
+# Moodle LMS security publications and acknowledgements page
Acknowledgments: https://moodle.org/security/
+
+# Submission preferred languages
Preferred-Languages: en
+
+# Moodle HQ canonical security.txt file
Canonical: https://moodle.org/.well-known/security.txt
+
+# Moodle LMS Security Procedures document, including Responsible Disclosure Policy
Policy: https://moodledev.io/general/development/process/security
-Expires: 2023-10-30T01:00:00.000Z
+
+# Expiry date of this document
+# This information is considered current and up to date until 3 weeks after the next major Moodle LMS release
+Expires: 2024-05-13T01:00:00.000Z
diff --git a/theme/boost/lib.php b/theme/boost/lib.php
index 7e98100f6d893..6cc398d535e92 100644
--- a/theme/boost/lib.php
+++ b/theme/boost/lib.php
@@ -135,7 +135,7 @@ function theme_boost_get_precompiled_css() {
* Get SCSS to prepend.
*
* @param theme_config $theme The theme config object.
- * @return array
+ * @return string
*/
function theme_boost_get_pre_scss($theme) {
global $CFG;
diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss
index 90805d167a73e..4a500c57bf22e 100644
--- a/theme/boost/scss/moodle/core.scss
+++ b/theme/boost/scss/moodle/core.scss
@@ -2087,7 +2087,7 @@ a.disabled {
text-decoration: none;
cursor: default;
font-style: italic;
- color: #808080;
+ color: $text-muted;
}
body.lockscroll {
diff --git a/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss
index 249fa732d42ed..6f12c9e5670d3 100644
--- a/theme/boost/scss/moodle/course.scss
+++ b/theme/boost/scss/moodle/course.scss
@@ -930,6 +930,9 @@ span.editinstructions {
border-left: calc(#{$list-group-border-width} + 5px) solid map-get($theme-colors, 'primary');
padding-left: calc(#{$list-group-item-padding-x} - 5px);
}
+ &:hover {
+ z-index: 2;
+ }
}
.item-actions {
diff --git a/theme/boost/scss/moodle/icons.scss b/theme/boost/scss/moodle/icons.scss
index fa6baf1999c7d..66b9456ea4644 100644
--- a/theme/boost/scss/moodle/icons.scss
+++ b/theme/boost/scss/moodle/icons.scss
@@ -137,6 +137,7 @@ $iconsizes: map-merge((
.activityicon,
.icon {
margin: 0;
+ font-size: 24px;
height: 24px;
width: 24px;
}
diff --git a/theme/boost/settings.php b/theme/boost/settings.php
index 70890ca68935a..5fa481f60c5d9 100644
--- a/theme/boost/settings.php
+++ b/theme/boost/settings.php
@@ -81,7 +81,6 @@
$setting->set_updatedcallback('theme_reset_all_caches');
$page->add($setting);
- // Variable $body-color.
// We use an empty default value because the default colour should come from the preset.
$name = 'theme_boost/brandcolor';
$title = get_string('brandcolor', 'theme_boost');
diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css
index 32ffee115e4cf..d86810e98e603 100644
--- a/theme/boost/style/moodle.css
+++ b/theme/boost/style/moodle.css
@@ -14809,7 +14809,7 @@ a.disabled {
text-decoration: none;
cursor: default;
font-style: italic;
- color: #808080;
+ color: #6a737b;
}
body.lockscroll {
@@ -15896,6 +15896,7 @@ blockquote {
.activityiconcontainer .activityicon,
.activityiconcontainer .icon {
margin: 0;
+ font-size: 24px;
height: 24px;
width: 24px;
}
@@ -18431,6 +18432,9 @@ span.editinstructions .alert-link {
border-left: calc(1px + 5px) solid #0f6cbf;
padding-left: calc(1.25rem - 5px);
}
+#course-category-listings .listitem:hover {
+ z-index: 2;
+}
#course-category-listings .item-actions {
margin-right: 1em;
display: inline-block;
diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css
index d0d24ab67d048..46086ea060a25 100644
--- a/theme/classic/style/moodle.css
+++ b/theme/classic/style/moodle.css
@@ -14809,7 +14809,7 @@ a.disabled {
text-decoration: none;
cursor: default;
font-style: italic;
- color: #808080;
+ color: #6a737b;
}
body.lockscroll {
@@ -15896,6 +15896,7 @@ blockquote {
.activityiconcontainer .activityicon,
.activityiconcontainer .icon {
margin: 0;
+ font-size: 24px;
height: 24px;
width: 24px;
}
@@ -18431,6 +18432,9 @@ span.editinstructions .alert-link {
border-left: calc(1px + 5px) solid #0f6cbf;
padding-left: calc(1.25rem - 5px);
}
+#course-category-listings .listitem:hover {
+ z-index: 2;
+}
#course-category-listings .item-actions {
margin-right: 1em;
display: inline-block;
diff --git a/user/editadvanced.php b/user/editadvanced.php
index 3a45e188d23b5..3d8795ebfef05 100644
--- a/user/editadvanced.php
+++ b/user/editadvanced.php
@@ -332,7 +332,8 @@
$streditmyprofile = get_string('editmyprofile');
$userfullname = fullname($user, true);
$PAGE->set_heading($userfullname);
- $PAGE->set_title("$course->shortname: $streditmyprofile - $userfullname");
+ $coursename = $course->id !== SITEID ? "$course->shortname" : '';
+ $PAGE->set_title("$streditmyprofile: $userfullname" . moodle_page::TITLE_SEPARATOR . $coursename);
echo $OUTPUT->header();
echo $OUTPUT->heading($userfullname);
}
diff --git a/user/emailupdate.php b/user/emailupdate.php
index 0577c68a0a9ec..6de500eff433d 100644
--- a/user/emailupdate.php
+++ b/user/emailupdate.php
@@ -42,7 +42,7 @@
$a->fullname = fullname($user, true);
$stremailupdate = get_string('emailupdate', 'auth', $a);
-$PAGE->set_title(format_string($SITE->fullname) . ": $stremailupdate");
+$PAGE->set_title($stremailupdate);
$PAGE->set_heading(format_string($SITE->fullname) . ": $stremailupdate");
if (empty($preferences['newemailattemptsleft'])) {
diff --git a/user/profile.php b/user/profile.php
index be5ce4851ae09..f4823f9875200 100644
--- a/user/profile.php
+++ b/user/profile.php
@@ -42,7 +42,9 @@
$edit = optional_param('edit', null, PARAM_BOOL); // Turn editing on and off.
$reset = optional_param('reset', null, PARAM_BOOL);
-$PAGE->set_url('/user/profile.php', array('id' => $userid));
+// Even if the user didn't supply a userid, we treat page URL as if they did; this is needed so navigation works correctly.
+$userid = $userid ?: $USER->id;
+$PAGE->set_url('/user/profile.php', ['id' => $userid]);
if (!empty($CFG->forceloginforprofiles)) {
require_login();
@@ -59,7 +61,6 @@
require_login();
}
-$userid = $userid ? $userid : $USER->id; // Owner of the page.
if ((!$user = $DB->get_record('user', array('id' => $userid))) || ($user->deleted)) {
$PAGE->set_context(context_system::instance());
echo $OUTPUT->header();
@@ -80,7 +81,7 @@
// Course managers can be browsed at site level. If not forceloginforprofiles, allow access (bug #4366).
$struser = get_string('user');
$PAGE->set_context(context_system::instance());
- $PAGE->set_title("$SITE->shortname: $struser"); // Do not leak the name.
+ $PAGE->set_title($struser); // Do not leak the name.
$PAGE->set_heading($struser);
$PAGE->set_pagelayout('mypublic');
$PAGE->add_body_class('limitedwidth');
diff --git a/user/profile/lib.php b/user/profile/lib.php
index b6a2cc21812a4..86058b71c7759 100644
--- a/user/profile/lib.php
+++ b/user/profile/lib.php
@@ -258,7 +258,7 @@ public function edit_validate_field($usernew) {
* @param MoodleQuickForm $mform instance of the moodleform class
*/
public function edit_field_set_default($mform) {
- if (!empty($this->field->defaultdata)) {
+ if (isset($this->field->defaultdata)) {
$mform->setDefault($this->inputname, $this->field->defaultdata);
}
}
diff --git a/user/repository.php b/user/repository.php
index d398a8380c7d9..444f35dc3a934 100644
--- a/user/repository.php
+++ b/user/repository.php
@@ -55,14 +55,11 @@
echo $OUTPUT->header();
-$currenttab = 'repositories';
-require('tabs.php');
-
echo $OUTPUT->heading($configstr);
echo $OUTPUT->box_start();
$params = array();
-$params['context'] = $coursecontext;
+$params['context'] = [$coursecontext];
$params['currentcontext'] = $PAGE->context;
$params['userid'] = $USER->id;
if (!$instances = repository::get_instances($params)) {
diff --git a/user/tests/behat/user_grade_navigation.feature b/user/tests/behat/user_grade_navigation.feature
index 83036308a5c83..77f19ec37b8af 100644
--- a/user/tests/behat/user_grade_navigation.feature
+++ b/user/tests/behat/user_grade_navigation.feature
@@ -22,18 +22,15 @@ Feature: The student can navigate to their grades page and user grade report.
| teacher1 | C1 | editingteacher |
| student1 | C2 | student |
And the following "activities" exist:
- | activity | course | idnumber | name | intro | grade |
- | assign | C1 | a1 | Test assignment one | Submit something! | 300 |
- | assign | C1 | a2 | Test assignment two | Submit something! | 100 |
- | assign | C1 | a3 | Test assignment three | Submit something! | 150 |
- | assign | C2 | a4 | Test assignment four | Submit something! | 150 |
- And I am on the "Course 1" course page logged in as teacher1
- And I navigate to "View > Grader report" in the course gradebook
- And I turn editing mode on
- And I give the grade "150.00" to the user "Student 1" for the grade item "Test assignment one"
- And I give the grade "67.00" to the user "Student 1" for the grade item "Test assignment two"
- And I press "Save changes"
- And I log out
+ | activity | course | idnumber | name | grade |
+ | assign | C1 | a1 | Test assignment one | 300 |
+ | assign | C1 | a2 | Test assignment two | 100 |
+ | assign | C1 | a3 | Test assignment three | 150 |
+ | assign | C2 | a4 | Test assignment four | 150 |
+ And the following "grade grades" exist:
+ | gradeitem | user | grade |
+ | Test assignment one | student1 | 150.00 |
+ | Test assignment two | student1 | 67.00 |
Scenario: Navigation to Grades and the user grade report.
When I log in as "student1"
diff --git a/version.php b/version.php
index 03471615dc8e8..66d42856f88f8 100644
--- a/version.php
+++ b/version.php
@@ -29,10 +29,10 @@
defined('MOODLE_INTERNAL') || die();
-$version = 2022112804.08; // 20221128 = branching date YYYYMMDD - do not modify!
+$version = 2022112805.09; // 20221128 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '4.1.4+ (Build: 20230728)'; // Human-friendly version name
-$release .= ' - GIP Recia 4.1.4.2';
+$release = '4.1.5+ (Build: 20230920)'; // Human-friendly version name
+$release .= ' - GIP Recia 4.1.5.1';
$branch = '401'; // This version's branch.
$maturity = MATURITY_STABLE; // This version's maturity level.
|