MDL-74610 quiz: multiple grades - setup for sections button

This commit is contained in:
Tim Hunt
2024-04-09 14:59:49 +01:00
parent 9a1b85aa4c
commit 48304bd2f8
12 changed files with 315 additions and 5 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+39
View File
@@ -35,6 +35,7 @@ import {replaceNode} from 'core/templates';
*/
const SELECTORS = {
'addGradeItemButton': '#mod_quiz-add_grade_item',
'autoSetupButton': '#mod_quiz-grades_auto_setup',
'editingPageContents': '#edit_grading_page-contents',
'gradeItemList': 'table#mod_quiz-grade-item-list',
'gradeItemSelect': 'select[data-slot-id]',
@@ -123,6 +124,21 @@ const updateSlotGradeItem = (
}
});
/**
* Call the Ajax service to setup one grade item for each quiz section.
*
* @param {Number} quizId id of the quiz to update.
* @return {Promise} Promise that resolves to the context required to re-render the page.
*/
const autoSetupGradeItems = (
quizId
) => callServiceAndReturnRenderingData({
methodname: 'mod_quiz_create_grade_item_per_section',
args: {
quizid: quizId
}
});
/**
* Make a web service call, and also call mod_quiz_get_edit_grading_page_data to get the date to re-render the page.
*
@@ -377,6 +393,9 @@ const handleButtonClick = (e) => {
if (e.target.closest(SELECTORS.addGradeItemButton)) {
handleAddGradeItemClick(e);
}
if (e.target.closest(SELECTORS.autoSetupButton)) {
handleAutoSetup(e);
}
if (e.target.closest(SELECTORS.resetAllButton)) {
handleResetAllClick(e);
}
@@ -404,6 +423,26 @@ const handleAddGradeItemClick = (e) => {
.catch(Notification.exception);
};
/**
* Handle clicks on the reset button - show a confirmation.
*
* @param {Event} e click event.
*/
const handleAutoSetup = (e) => {
e.preventDefault();
const pending = new Pending('setup-quiz-grade-items');
const quizId = e.target.dataset.quizId;
autoSetupGradeItems(quizId)
.then(reRenderPage)
.then(() => {
pending.resolve();
document.querySelector(SELECTORS.resetAllButton).focus();
})
.catch(Notification.exception);
};
/**
* Handle clicks on the reset button - show a confirmation.
*
@@ -0,0 +1,110 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use coding_exception;
use core_external\external_api;
use core_external\external_description;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use moodle_exception;
use stdClass;
/**
* For a quiz with no grade items yet, create a grade item for each section.
*
* And, assign the questions in each section to the corresponding grade item.
*
* The user must have the 'mod/quiz:manage' capability for the quiz.
*
* @package mod_quiz
* @copyright 2024 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class create_grade_item_per_section extends external_api {
/**
* Declare the method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'quizid' => new external_value(PARAM_INT, 'The quiz to update slots for.'),
]);
}
/**
* For a quiz with no grade items yet, create a grade item for each section.
*
* And, assign the questions in each section to the corresponding grade item.
*
* The user must have the 'mod/quiz:manage' capability for the quiz.
*
* @param int $quizid the id of the quiz to setup grade items for.
*/
public static function execute(int $quizid): void {
global $DB;
[
'quizid' => $quizid,
] = self::validate_parameters(self::execute_parameters(), [
'quizid' => $quizid,
]);
// Check the request is valid.
$quizobj = quiz_settings::create($quizid);
require_capability('mod/quiz:manage', $quizobj->get_context());
self::validate_context($quizobj->get_context());
$structure = $quizobj->get_structure();
if ($structure->get_grade_items()) {
throw new coding_exception('Cannot use create_grade_item_per_section for a quiz ' .
'that already has grade items.');
}
$transaction = $DB->start_delegated_transaction();
$gradeitemsids = [];
foreach ($structure->get_sections() as $section) {
$gradeitem = new stdClass();
$gradeitem->quizid = $quizid;
$gradeitem->name = $section->heading;
$structure->create_grade_item($gradeitem);
$gradeitemsids[$section->id] = $gradeitem->id;
}
foreach ($structure->get_slots() as $slot) {
$structure->update_slot_grade_item($slot, $gradeitemsids[$slot->section->id]);
}
$transaction->allow_commit();
}
/**
* Define the webservice response.
*
* @return external_description|null always null.
*/
public static function execute_returns(): ?external_description {
return null;
}
}
@@ -112,6 +112,7 @@ class edit_grading_page implements renderable, templatable {
'gradeitems' => $gradeitems,
'hasslots' => $this->structure->has_questions(),
'sections' => array_values($sections),
'hasmultiplesections' => count($sections) > 1,
'nogradeitems' => ['message' => get_string('gradeitemsnoneyet', 'quiz')],
'noslots' => ['message' => get_string('gradeitemnoslots', 'quiz')],
];
+2 -3
View File
@@ -55,8 +55,7 @@ class structure {
protected $slotsinorder = [];
/**
* @var stdClass[] currently a dummy. Holds data that will match the
* quiz_sections, once it exists.
* @var stdClass[] this quiz's data from the quiz_sections table. Each item has a ->lastslot field too.
*/
protected $sections = [];
@@ -1505,7 +1504,7 @@ class structure {
*
* The new grade item is added at the end of the order.
*
* @param stdClass $gradeitemdata must have property name.
* @param stdClass $gradeitemdata must have property name - updated with the inserted data (sortorder and id).
*/
public function create_grade_item(stdClass $gradeitemdata): void {
global $DB;
+8
View File
@@ -295,4 +295,12 @@ $functions = [
'capabilities' => 'mod/quiz:manage',
'ajax' => true,
],
'mod_quiz_create_grade_item_per_section' => [
'classname' => 'mod_quiz\external\create_grade_item_per_section',
'description' => 'For a quiz with no grade items yet, create a grade item for each section, with the questions in that section assigned.',
'type' => 'write',
'capabilities' => 'mod/quiz:manage',
'ajax' => true,
],
];
+1
View File
@@ -448,6 +448,7 @@ $string['gradeitemnewname'] = 'New name for grade item {$a}';
$string['gradeitemnoneselected'] = '[none]';
$string['gradeitemnoslots'] = 'No questions have been added to the quiz yet. Please add the questions to the quiz before setting up grading.';
$string['gradeitems'] = 'Grade items';
$string['gradeitemsautosetup'] = 'Setup a grade for each section';
$string['gradeitemsetup'] = 'Quiz grading setup (advanced view)';
$string['gradeitemsnoneyet'] = 'This quiz does not yet have any grade items defined, just a simple overall score will be used.';
$string['gradeitemsremoveall'] = 'Reset advanced grading setup';
@@ -135,6 +135,12 @@
<button type="button" class="btn btn-danger" id="mod_quiz-grades_reset_all"
data-quiz-id="{{quizid}}">{{#str}} gradeitemsremoveall, quiz{{/str}}</button>
{{/hasgradeitems}}
{{^hasgradeitems}}
{{#hasmultiplesections}}
<button type="button" class="btn btn-secondary" id="mod_quiz-grades_auto_setup"
data-quiz-id="{{quizid}}">{{#str}} gradeitemsautosetup, quiz{{/str}}</button>
{{/hasmultiplesections}}
{{/hasgradeitems}}
</div>
<h3>{{#str}} gradeitemmarkscheme, quiz {{/str}}</h3>
@@ -0,0 +1,53 @@
@mod @mod_quiz
Feature: Attempt a quiz with multiple grades
As a student
In order to demonstrate multiple skills at once
I need to be able to attempt quizzes with multiple grades setup
Background:
Given the following "users" exist:
| username |
| student |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "activities" exist:
| activity | name | course |
| quiz | Quiz 1 | C1 |
@javascript
Scenario: Navigation to, and display of, grading setup
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Reading | Can you read this? |
| Test questions | truefalse | Listening | Can you hear this? |
And the following "mod_quiz > grade items" exist:
| quiz | name |
| Quiz 1 | Grade for reading |
| Quiz 1 | Grade for listening |
| Quiz 1 | Unused grade item |
And quiz "Quiz 1" contains the following questions:
| question | page | grade item |
| Reading | 1 | Grade for reading |
| Listening | 1 | Grade for listening |
When I am on the "Quiz 1" "quiz activity" page logged in as "student"
And I click on "Attempt quiz" "button"
And I set the field "True" in the "Can you read this?" "question" to "1"
And I set the field "False" in the "Can you hear this?" "question" to "1"
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"
Then I should see "1.00 out of 1.00" in the "Grade for reading" "table_row"
And I should see "0.00 out of 1.00" in the "Grade for listening" "table_row"
And I should not see "Unused grade item"
And I should see "1.00/2.00" in the "Marks" "table_row"
# Funny order because 'Grade' also appears in other rows.
And I should see "Grade" in the "50.00 out of 100.00" "table_row"
@@ -148,3 +148,24 @@ Feature: Setup multiple grades for a quiz
And I press "Reset advanced grading setup"
And I click on "Cancel" "button" in the "Reset grading setup?" "dialogue"
Then I should see "Intuition"
@javascript
Scenario: Automatically set up one grade item per section
Given quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 1 |
| Question C | 2 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Reading | 1 | 0 |
| Listening | 3 | 0 |
When I am on the "Quiz 1" "mod_quiz > multiple grades setup" page logged in as teacher
And I press "Setup a grade for each section"
Then "Reading" "table_row" should exist in the "mod_quiz-grade-item-list" "table"
And "Listening" "table_row" should exist in the "mod_quiz-grade-item-list" "table"
And the field "Question A" matches value "Reading"
And the field "Question B" matches value "Reading"
And the field "Question C" matches value "Listening"
+72
View File
@@ -200,4 +200,76 @@ final class grade_items_test extends externallib_advanced_testcase {
return $quizobj;
}
public function test_create_grade_item_per_section_works(): void {
global $SITE;
$this->resetAfterTest();
$this->setAdminUser();
// Create a quiz with no grade items yet, but two sections.
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id]);
// Create three questions.
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$saq1 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$saq2 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$saq3 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
// Add them to the quiz.
quiz_add_quiz_question($saq1->id, $quiz, 1, 1);
quiz_add_quiz_question($saq2->id, $quiz, 1, 1);
quiz_add_quiz_question($saq3->id, $quiz, 2, 1);
// Create two sections.
$quizobj = quiz_settings::create($quiz->id);
$structure = $quizobj->get_structure();
$defaultsection = array_values($structure->get_sections())[0];
$structure->set_section_heading($defaultsection->id, 'Listening');
$structure->add_section_heading(2, 'Reading');
// Call the method we are testing.
create_grade_item_per_section::execute($quizobj->get_quizid());
// Verify.
$structure = $quizobj->get_structure();
$gradeitems = array_values($structure->get_grade_items());
$this->assertCount(2, $gradeitems);
$this->assertEquals('Listening', $gradeitems[0]->name);
$this->assertEquals(1, $gradeitems[0]->sortorder);
$this->assertEquals('Reading', $gradeitems[1]->name);
$this->assertEquals(2, $gradeitems[1]->sortorder);
$this->assertEquals($gradeitems[0]->id, $structure->get_slot_by_number(1)->quizgradeitemid);
$this->assertEquals($gradeitems[0]->id, $structure->get_slot_by_number(2)->quizgradeitemid);
$this->assertEquals($gradeitems[1]->id, $structure->get_slot_by_number(3)->quizgradeitemid);
}
public function test_create_grade_item_per_section_service_checks_permissions(): void {
global $SITE;
$this->resetAfterTest();
// Create a quiz.
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id]);
$unprivilegeduser = $this->getDataGenerator()->create_user();
$this->setUser($unprivilegeduser);
$this->expectException(required_capability_exception::class);
create_grade_item_per_section::execute($quiz->id);
}
public function test_cant_create_grade_item_per_section_if_grade_items_already_exist(): void {
$quizobj = $this->create_quiz_with_two_grade_items();
$this->expectException(coding_exception::class);
create_grade_item_per_section::execute($quizobj->get_quizid());
}
}