first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+574
View File
@@ -0,0 +1,574 @@
<?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;
use core_question\local\bank\condition;
use core_question\local\bank\question_version_status;
use core_question_generator;
use mod_quiz_generator;
use question_engine;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Tests for the quiz_attempt class.
*
* @package mod_quiz
* @category test
* @copyright 2014 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\quiz_attempt
*/
class attempt_test extends \advanced_testcase {
/**
* Create quiz and attempt data with layout.
*
* @param string $layout layout to set. Like quiz attempt.layout. E.g. '1,2,0,3,4,0,'.
* @param string $navmethod quiz navigation method (defaults to free)
* @return quiz_attempt the new quiz_attempt object
*/
protected function create_quiz_and_attempt_with_layout($layout, $navmethod = QUIZ_NAVMETHOD_FREE) {
$this->resetAfterTest(true);
// Make a user to do the quiz.
$user = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id,
'grade' => 100.0, 'sumgrades' => 2, 'navmethod' => $navmethod]);
$quizobj = quiz_settings::create($quiz->id, $user->id);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$page = 1;
foreach (explode(',', $layout) as $slot) {
if ($slot == 0) {
$page += 1;
continue;
}
$question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz, $page);
}
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null, false, [], [], $user->id);
return quiz_attempt::create($attempt->id);
}
public function test_attempt_url(): void {
$attempt = $this->create_quiz_and_attempt_with_layout('1,2,0,3,4,0,5,6,0');
$attemptid = $attempt->get_attempt()->id;
$cmid = $attempt->get_cmid();
$url = '/mod/quiz/attempt.php';
$params = ['attempt' => $attemptid, 'cmid' => $cmid, 'page' => 2];
$this->assertEquals(new \moodle_url($url, $params), $attempt->attempt_url(null, 2));
$params['page'] = 1;
$this->assertEquals(new \moodle_url($url, $params), $attempt->attempt_url(3));
$questionattempt = $attempt->get_question_attempt(4);
$expecteanchor = $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url($url, $params, $expecteanchor), $attempt->attempt_url(4));
$questionattempt = $attempt->get_question_attempt(3);
$expecteanchor = '#' . $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url('#'), $attempt->attempt_url(null, 2, 2));
$this->assertEquals(new \moodle_url($expecteanchor), $attempt->attempt_url(3, -1, 1));
$questionattempt = $attempt->get_question_attempt(4);
$expecteanchor = $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url(null, null, $expecteanchor, null), $attempt->attempt_url(4, -1, 1));
// Summary page.
$url = '/mod/quiz/summary.php';
unset($params['page']);
$this->assertEquals(new \moodle_url($url, $params), $attempt->summary_url());
// Review page.
$url = '/mod/quiz/review.php';
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url());
$params['page'] = 1;
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(3, -1, false));
$this->assertEquals(new \moodle_url($url, $params, $expecteanchor), $attempt->review_url(4, -1, false));
unset($params['page']);
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 2, true));
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(1, -1, true));
$params['page'] = 2;
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 2, false));
unset($params['page']);
$params['showall'] = 0;
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 0, false));
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(1, -1, false));
$params['page'] = 1;
unset($params['showall']);
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(3, -1, false));
$params['page'] = 2;
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 2));
$this->assertEquals(new \moodle_url('#'), $attempt->review_url(null, -1, null, 0));
$questionattempt = $attempt->get_question_attempt(3);
$expecteanchor = '#' . $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url($expecteanchor), $attempt->review_url(3, -1, null, 0));
$questionattempt = $attempt->get_question_attempt(4);
$expecteanchor = '#' . $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url($expecteanchor), $attempt->review_url(4, -1, null, 0));
$this->assertEquals(new \moodle_url('#'), $attempt->review_url(null, 2, true, 0));
$questionattempt = $attempt->get_question_attempt(1);
$expecteanchor = '#' . $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url($expecteanchor), $attempt->review_url(1, -1, true, 0));
$this->assertEquals(new \moodle_url($expecteanchor), $attempt->review_url(1, -1, false, 0));
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 2, false, 0));
$this->assertEquals(new \moodle_url('#'), $attempt->review_url(null, 0, false, 0));
$params['page'] = 1;
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(3, -1, false, 0));
// Setup another attempt.
$attempt = $this->create_quiz_and_attempt_with_layout(
'1,2,3,4,5,6,7,8,9,10,0,11,12,13,14,15,16,17,18,19,20,0,' .
'21,22,23,24,25,26,27,28,29,30,0,31,32,33,34,35,36,37,38,39,40,0,' .
'41,42,43,44,45,46,47,48,49,50,0,51,52,53,54,55,56,57,58,59,60,0');
$attemptid = $attempt->get_attempt()->id;
$cmid = $attempt->get_cmid();
$params = ['attempt' => $attemptid, 'cmid' => $cmid];
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url());
$params['page'] = 2;
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 2));
$params['page'] = 1;
unset($params['showall']);
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(11, -1, false));
$questionattempt = $attempt->get_question_attempt(12);
$expecteanchor = $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url($url, $params, $expecteanchor), $attempt->review_url(12, -1, false));
$params['showall'] = 1;
unset($params['page']);
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 2, true));
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(1, -1, true));
$params['page'] = 2;
unset($params['showall']);
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 2, false));
unset($params['page']);
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 0, false));
$params['page'] = 1;
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(11, -1, false));
$this->assertEquals(new \moodle_url($url, $params, $expecteanchor), $attempt->review_url(12, -1, false));
$params['page'] = 2;
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 2));
$this->assertEquals(new \moodle_url('#'), $attempt->review_url(null, -1, null, 0));
$questionattempt = $attempt->get_question_attempt(3);
$expecteanchor = $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url(null, null, $expecteanchor), $attempt->review_url(3, -1, null, 0));
$questionattempt = $attempt->get_question_attempt(4);
$expecteanchor = $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url(null, null, $expecteanchor), $attempt->review_url(4, -1, null, 0));
$questionattempt = $attempt->get_question_attempt(1);
$expecteanchor = '#' . $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url($expecteanchor), $attempt->review_url(1, -1, true, 0));
$this->assertEquals(new \moodle_url('#'), $attempt->review_url(null, 2, true, 0));
$params['page'] = 2;
$questionattempt = $attempt->get_question_attempt(1);
$expecteanchor = '#' . $questionattempt->get_outer_question_div_unique_id();
$this->assertEquals(new \moodle_url($expecteanchor), $attempt->review_url(1, -1, false, 0));
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(null, 2, false, 0));
$this->assertEquals(new \moodle_url('#'), $attempt->review_url(null, 0, false, 0));
$params['page'] = 1;
$this->assertEquals(new \moodle_url($url, $params), $attempt->review_url(11, -1, false, 0));
}
/**
* Tests attempt page titles when all questions are on a single page.
*/
public function test_attempt_titles_single(): void {
$attempt = $this->create_quiz_and_attempt_with_layout('1,2,0');
// Attempt page.
$this->assertEquals('Quiz 1', $attempt->attempt_page_title(0));
// Summary page.
$this->assertEquals('Quiz 1: Attempt summary', $attempt->summary_page_title());
// Review page.
$this->assertEquals('Quiz 1: Attempt review', $attempt->review_page_title(0));
}
/**
* Tests attempt page titles when questions are on multiple pages, but are reviewed on a single page.
*/
public function test_attempt_titles_multiple_single(): void {
$attempt = $this->create_quiz_and_attempt_with_layout('1,2,0,3,4,0,5,6,0');
// Attempt page.
$this->assertEquals('Quiz 1 (page 1 of 3)', $attempt->attempt_page_title(0));
$this->assertEquals('Quiz 1 (page 2 of 3)', $attempt->attempt_page_title(1));
$this->assertEquals('Quiz 1 (page 3 of 3)', $attempt->attempt_page_title(2));
// Summary page.
$this->assertEquals('Quiz 1: Attempt summary', $attempt->summary_page_title());
// Review page.
$this->assertEquals('Quiz 1: Attempt review', $attempt->review_page_title(0, true));
}
/**
* Tests attempt page titles when questions are on multiple pages, and they are reviewed on multiple pages as well.
*/
public function test_attempt_titles_multiple_multiple(): void {
$attempt = $this->create_quiz_and_attempt_with_layout(
'1,2,3,4,5,6,7,8,9,10,0,11,12,13,14,15,16,17,18,19,20,0,' .
'21,22,23,24,25,26,27,28,29,30,0,31,32,33,34,35,36,37,38,39,40,0,' .
'41,42,43,44,45,46,47,48,49,50,0,51,52,53,54,55,56,57,58,59,60,0');
// Attempt page.
$this->assertEquals('Quiz 1 (page 1 of 6)', $attempt->attempt_page_title(0));
$this->assertEquals('Quiz 1 (page 2 of 6)', $attempt->attempt_page_title(1));
$this->assertEquals('Quiz 1 (page 6 of 6)', $attempt->attempt_page_title(5));
// Summary page.
$this->assertEquals('Quiz 1: Attempt summary', $attempt->summary_page_title());
// Review page.
$this->assertEquals('Quiz 1: Attempt review (page 1 of 6)', $attempt->review_page_title(0));
$this->assertEquals('Quiz 1: Attempt review (page 2 of 6)', $attempt->review_page_title(1));
$this->assertEquals('Quiz 1: Attempt review (page 6 of 6)', $attempt->review_page_title(5));
// When all questions are shown.
$this->assertEquals('Quiz 1: Attempt review', $attempt->review_page_title(0, true));
$this->assertEquals('Quiz 1: Attempt review', $attempt->review_page_title(1, true));
}
public function test_is_participant(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$student2 = $this->getDataGenerator()->create_and_enrol($course, 'student', [], 'manual', 0, 0, ENROL_USER_SUSPENDED);
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$quizobj = quiz_settings::create($quiz->id);
// Login as student.
$this->setUser($student);
// Convert to a lesson object.
$this->assertEquals(true, $quizobj->is_participant($student->id),
'Student is enrolled, active and can participate');
// Login as student2.
$this->setUser($student2);
$this->assertEquals(false, $quizobj->is_participant($student2->id),
'Student is enrolled, suspended and can NOT participate');
// Login as an admin.
$this->setAdminUser();
$this->assertEquals(false, $quizobj->is_participant($USER->id),
'Admin is not enrolled and can NOT participate');
$this->getDataGenerator()->enrol_user(2, $course->id);
$this->assertEquals(true, $quizobj->is_participant($USER->id),
'Admin is enrolled and can participate');
$this->getDataGenerator()->enrol_user(2, $course->id, [], 'manual', 0, 0, ENROL_USER_SUSPENDED);
$this->assertEquals(true, $quizobj->is_participant($USER->id),
'Admin is enrolled, suspended and can participate');
}
/**
* Test quiz_prepare_and_start_new_attempt function
*/
public function test_quiz_prepare_and_start_new_attempt(): void {
global $USER;
$this->resetAfterTest();
// Create course.
$course = $this->getDataGenerator()->create_course();
// Create students.
$student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
// Create question and add it to quiz.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz, 1);
$quizobj = quiz_settings::create($quiz->id);
// Login as student1.
$this->setUser($student1);
// Create attempt for student1.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null, false, [], []);
$this->assertEquals($student1->id, $attempt->userid);
$this->assertEquals(0, $attempt->preview);
// Login as student2.
$this->setUser($student2);
// Create attempt for student2.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null, false, [], []);
$this->assertEquals($student2->id, $attempt->userid);
$this->assertEquals(0, $attempt->preview);
// Login as admin.
$this->setAdminUser();
// Create attempt for student1.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 2, null, false, [], [], $student1->id);
$this->assertEquals($student1->id, $attempt->userid);
$this->assertEquals(0, $attempt->preview);
$student1attempt = $attempt; // Save for extra verification below.
// Create attempt for student2.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 2, null, false, [], [], $student2->id);
$this->assertEquals($student2->id, $attempt->userid);
$this->assertEquals(0, $attempt->preview);
// Create attempt for user id that the same with current $USER->id.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 2, null, false, [], [], $USER->id);
$this->assertEquals($USER->id, $attempt->userid);
$this->assertEquals(1, $attempt->preview);
// Check that the userid stored in the first step is the user the attempt is for,
// not the user who triggered the creation.
$quba = question_engine::load_questions_usage_by_activity($student1attempt->uniqueid);
$step = $quba->get_question_attempt(1)->get_step(0);
$this->assertEquals($student1->id, $step->get_user_id());
}
/**
* Test quiz_prepare_and_start_new_attempt function
*/
public function test_quiz_prepare_and_start_new_attempt_random_draft(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create course.
$course = $this->getDataGenerator()->create_course();
// Create quiz.
/** @var mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id]);
// Create question with 2 versions. V1 ready. V2 draft.
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('shortanswer', null,
['questiontext' => 'V1', 'category' => $category->id]);
$questiongenerator->update_question($question, null,
['questiontext' => 'V2', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
// Add a random question form that category.
$filtercondition = [
'filter' => [
'category' => [
'jointype' => condition::JOINTYPE_DEFAULT,
'values' => [$category->id],
'filteroptions' => ['includesubcategories' => false],
],
],
];
$quizobj = quiz_settings::create($quiz->id);
$quizobj->get_structure()->add_random_questions(1, 1, $filtercondition);
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
// Create an attempt.
$quizobj = quiz_settings::create($quiz->id);
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
$this->assertEquals(1, $attempt->preview);
}
/**
* Test check_page_access function
* @covers \quiz_attempt::check_page_access
*/
public function test_check_page_access(): void {
$timenow = time();
// Free navigation.
$attempt = $this->create_quiz_and_attempt_with_layout('1,0,2,0,3,0,4,0,5,0', QUIZ_NAVMETHOD_FREE);
// Check access.
$this->assertTrue($attempt->check_page_access(4));
$this->assertTrue($attempt->check_page_access(3));
$this->assertTrue($attempt->check_page_access(2));
$this->assertTrue($attempt->check_page_access(1));
$this->assertTrue($attempt->check_page_access(0));
$this->assertTrue($attempt->check_page_access(2));
// Access page 2.
$attempt->set_currentpage(2);
$attempt = quiz_attempt::create($attempt->get_attempt()->id);
// Check access.
$this->assertTrue($attempt->check_page_access(0));
$this->assertTrue($attempt->check_page_access(1));
$this->assertTrue($attempt->check_page_access(2));
$this->assertTrue($attempt->check_page_access(3));
$this->assertTrue($attempt->check_page_access(4));
// Sequential navigation.
$attempt = $this->create_quiz_and_attempt_with_layout('1,0,2,0,3,0,4,0,5,0', QUIZ_NAVMETHOD_SEQ);
// Check access.
$this->assertTrue($attempt->check_page_access(0));
$this->assertTrue($attempt->check_page_access(1));
$this->assertFalse($attempt->check_page_access(2));
$this->assertFalse($attempt->check_page_access(3));
$this->assertFalse($attempt->check_page_access(4));
// Access page 1.
$attempt->set_currentpage(1);
$attempt = quiz_attempt::create($attempt->get_attempt()->id);
$this->assertTrue($attempt->check_page_access(1));
// Access page 2.
$attempt->set_currentpage(2);
$attempt = quiz_attempt::create($attempt->get_attempt()->id);
$this->assertTrue($attempt->check_page_access(2));
$this->assertTrue($attempt->check_page_access(3));
$this->assertFalse($attempt->check_page_access(4));
$this->assertFalse($attempt->check_page_access(1));
}
/**
* Starting a new attempt with a question in draft status should throw an exception.
*
* @covers ::quiz_start_new_attempt()
* @return void
*/
public function test_start_new_attempt_with_draft(): void {
$this->resetAfterTest();
// Create course.
$course = $this->getDataGenerator()->create_course();
// Create students.
$student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
// Create question and add it to quiz.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('shortanswer', null,
['category' => $cat->id, 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
quiz_add_quiz_question($question->id, $quiz, 1);
$quizobj = quiz_settings::create($quiz->id);
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$attempt = quiz_create_attempt($quizobj, 1, false, time(), false, $student1->id);
$this->expectExceptionObject(new \moodle_exception('questiondraftonly', 'mod_quiz', '', $question->name));
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, time());
}
/**
* Starting a new attempt built on last with a question in draft status should throw an exception.
*
* @covers ::quiz_start_attempt_built_on_last()
* @return void
*/
public function test_quiz_start_attempt_built_on_last_with_draft(): void {
global $DB;
$this->resetAfterTest();
// Create course.
$course = $this->getDataGenerator()->create_course();
// Create students.
$student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
// Create question and add it to quiz.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz, 1);
$quizobj = quiz_settings::create($quiz->id);
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$attempt = quiz_create_attempt($quizobj, 1, false, time(), false, $student1->id);
$attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, 1, time());
$attempt = quiz_attempt_save_started($quizobj, $quba, $attempt);
$DB->set_field('question_versions', 'status', question_version_status::QUESTION_STATUS_DRAFT,
['questionid' => $question->id]);
// We need to reset the cache since the question has been edited by changing its status to draft.
\question_bank::notify_question_edited($question->id);
$quizobj = quiz_settings::create($quiz->id);
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$newattempt = quiz_create_attempt($quizobj, 2, $attempt, time(), false, $student1->id);
$this->expectExceptionObject(new \moodle_exception('questiondraftonly', 'mod_quiz', '', $question->name));
quiz_start_attempt_built_on_last($quba, $newattempt, $attempt);
}
public function test_get_grade_item_totals(): void {
$attemptobj = $this->create_quiz_and_attempt_with_layout('1,2,3,0');
/** @var mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
// Set up some section grades.
$listeninggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Listening']);
$readinggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Reading']);
$structure = $attemptobj->get_quizobj()->get_structure();
$structure->update_slot_grade_item($structure->get_slot_by_number(1), $listeninggrade->id);
$structure->update_slot_grade_item($structure->get_slot_by_number(2), $listeninggrade->id);
$structure->update_slot_grade_item($structure->get_slot_by_number(3), $readinggrade->id);
// Reload the attempt and verify.
$attemptobj = quiz_attempt::create($attemptobj->get_attemptid());
$grades = $attemptobj->get_grade_item_totals();
// All grades zero because student has not done the quiz yet, but this is a sufficent test.
$this->assertEquals('Listening', $grades[$listeninggrade->id]->name);
$this->assertEquals(0, $grades[$listeninggrade->id]->grade);
$this->assertEquals(2, $grades[$listeninggrade->id]->maxgrade);
$this->assertEquals('Reading', $grades[$readinggrade->id]->name);
$this->assertEquals(0, $grades[$readinggrade->id]->grade);
$this->assertEquals(1, $grades[$readinggrade->id]->maxgrade);
}
}
@@ -0,0 +1,368 @@
<?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;
use question_engine;
use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Quiz attempt walk through using data from csv file.
*
* @package mod_quiz
* @category test
* @copyright 2013 The Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_walkthrough_from_csv_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* @var string[] names of the files which contain the test data.
*/
protected $files = ['questions', 'steps', 'results'];
/**
* @var stdClass the quiz record we create.
*/
protected $quiz;
/**
* @var array with slot no => question name => questionid. Question ids of questions created in the same category as random q.
*/
protected $randqids;
/**
* The only test in this class. This is run multiple times depending on how many sets of files there are in fixtures/
* directory.
*
* @param array $quizsettings of settings read from csv file quizzes.csv
* @param array $csvdata of data read from csv file "questionsXX.csv", "stepsXX.csv" and "resultsXX.csv".
* @dataProvider get_data_for_walkthrough
*/
public function test_walkthrough_from_csv($quizsettings, $csvdata): void {
// CSV data files for these tests were generated using :
// https://github.com/jamiepratt/moodle-quiz-tools/tree/master/responsegenerator
$this->create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata);
}
public function create_quiz($quizsettings, $qs) {
global $SITE, $DB;
$this->setAdminUser();
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$slots = [];
$qidsbycat = [];
$sumofgrades = 0;
foreach ($qs as $qsrow) {
$q = $this->explode_dot_separated_keys_to_make_subindexs($qsrow);
$catname = ['name' => $q['cat']];
if (!$cat = $DB->get_record('question_categories', ['name' => $q['cat']])) {
$cat = $questiongenerator->create_question_category($catname);
}
$q['catid'] = $cat->id;
foreach (['which' => null, 'overrides' => []] as $key => $default) {
if (empty($q[$key])) {
$q[$key] = $default;
}
}
if ($q['type'] !== 'random') {
// Don't actually create random questions here.
$overrides = ['category' => $cat->id, 'defaultmark' => $q['mark']] + $q['overrides'];
if ($q['type'] === 'truefalse') {
// True/false question can never have hints, but sometimes we need to put them
// in the CSV file, to keep it rectangular.
unset($overrides['hint']);
}
$question = $questiongenerator->create_question($q['type'], $q['which'], $overrides);
$q['id'] = $question->id;
if (!isset($qidsbycat[$q['cat']])) {
$qidsbycat[$q['cat']] = [];
}
if (!empty($q['which'])) {
$name = $q['type'].'_'.$q['which'];
} else {
$name = $q['type'];
}
$qidsbycat[$q['catid']][$name] = $q['id'];
}
if (!empty($q['slot'])) {
$slots[$q['slot']] = $q;
$sumofgrades += $q['mark'];
}
}
ksort($slots);
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
// Settings from param override defaults.
$aggregratedsettings = $quizsettings + ['course' => $SITE->id,
'questionsperpage' => 0,
'grade' => 100.0,
'sumgrades' => $sumofgrades];
$this->quiz = $quizgenerator->create_instance($aggregratedsettings);
$this->randqids = [];
foreach ($slots as $slotno => $slotquestion) {
if ($slotquestion['type'] !== 'random') {
quiz_add_quiz_question($slotquestion['id'], $this->quiz, 0, $slotquestion['mark']);
} else {
$this->add_random_questions($this->quiz->id, 0, $slotquestion['catid'], 1);
$this->randqids[$slotno] = $qidsbycat[$slotquestion['catid']];
}
}
}
/**
* Create quiz, simulate attempts and check results (if resultsXX.csv exists).
*
* @param array $quizsettings Quiz overrides for this quiz.
* @param array $csvdata Data loaded from csv files for this test.
*/
protected function create_quiz_simulate_attempts_and_check_results(array $quizsettings, array $csvdata) {
$this->resetAfterTest();
$this->create_quiz($quizsettings, $csvdata['questions']);
$attemptids = $this->walkthrough_attempts($csvdata['steps']);
if (isset($csvdata['results'])) {
$this->check_attempts_results($csvdata['results'], $attemptids);
}
}
/**
* Get full path of CSV file.
*
* @param string $setname
* @param string $test
* @return string full path of file.
*/
protected function get_full_path_of_csv_file(string $setname, string $test): string {
return __DIR__."/fixtures/{$setname}{$test}.csv";
}
/**
* Load dataset from CSV file "{$setname}{$test}.csv".
*
* @param string $setname
* @param string $test
* @return array
*/
protected function load_csv_data_file(string $setname, string $test = ''): array {
$files = [$setname => $this->get_full_path_of_csv_file($setname, $test)];
return $this->dataset_from_files($files)->get_rows([$setname]);
}
/**
* Break down row of csv data into sub arrays, according to column names.
*
* @param array $row from csv file with field names with parts separate by '.'.
* @return array the row with each part of the field name following a '.' being a separate sub array's index.
*/
protected function explode_dot_separated_keys_to_make_subindexs(array $row): array {
$parts = [];
foreach ($row as $columnkey => $value) {
$newkeys = explode('.', trim($columnkey));
$placetoputvalue =& $parts;
foreach ($newkeys as $newkeydepth => $newkey) {
if ($newkeydepth + 1 === count($newkeys)) {
$placetoputvalue[$newkey] = $value;
} else {
// Going deeper down.
if (!isset($placetoputvalue[$newkey])) {
$placetoputvalue[$newkey] = [];
}
$placetoputvalue =& $placetoputvalue[$newkey];
}
}
}
return $parts;
}
/**
* Data provider method for test_walkthrough_from_csv. Called by PHPUnit.
*
* @return array One array element for each run of the test. Each element contains an array with the params for
* test_walkthrough_from_csv.
*/
public function get_data_for_walkthrough(): array {
$quizzes = $this->load_csv_data_file('quizzes')['quizzes'];
$datasets = [];
foreach ($quizzes as $quizsettings) {
$dataset = [];
foreach ($this->files as $file) {
if (file_exists($this->get_full_path_of_csv_file($file, $quizsettings['testnumber']))) {
$dataset[$file] = $this->load_csv_data_file($file, $quizsettings['testnumber'])[$file];
}
}
$datasets[] = [$quizsettings, $dataset];
}
return $datasets;
}
/**
* @param array $steps the step data from the csv file.
* @return array attempt no as in csv file => the id of the quiz_attempt as stored in the db.
*/
protected function walkthrough_attempts(array $steps): array {
global $DB;
$attemptids = [];
foreach ($steps as $steprow) {
$step = $this->explode_dot_separated_keys_to_make_subindexs($steprow);
// Find existing user or make a new user to do the quiz.
$username = ['firstname' => $step['firstname'],
'lastname' => $step['lastname']];
if (!$user = $DB->get_record('user', $username)) {
$user = $this->getDataGenerator()->create_user($username);
}
if (!isset($attemptids[$step['quizattempt']])) {
// Start the attempt.
$quizobj = quiz_settings::create($this->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);
$prevattempts = quiz_get_user_attempts($this->quiz->id, $user->id, 'all', true);
$attemptnumber = count($prevattempts) + 1;
$timenow = time();
$attempt = quiz_create_attempt($quizobj, $attemptnumber, null, $timenow, false, $user->id);
// Select variant and / or random sub question.
if (!isset($step['variants'])) {
$step['variants'] = [];
}
if (isset($step['randqs'])) {
// Replace 'names' with ids.
foreach ($step['randqs'] as $slotno => $randqname) {
$step['randqs'][$slotno] = $this->randqids[$slotno][$randqname];
}
} else {
$step['randqs'] = [];
}
quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $step['randqs'], $step['variants']);
quiz_attempt_save_started($quizobj, $quba, $attempt);
$attemptid = $attemptids[$step['quizattempt']] = $attempt->id;
} else {
$attemptid = $attemptids[$step['quizattempt']];
}
// Process some responses from the student.
$attemptobj = quiz_attempt::create($attemptid);
$attemptobj->process_submitted_actions($timenow, false, $step['responses']);
// Finish the attempt.
if (!isset($step['finished']) || ($step['finished'] == 1)) {
$attemptobj = quiz_attempt::create($attemptid);
$attemptobj->process_finish($timenow, false);
}
}
return $attemptids;
}
/**
* @param array $results the results data from the csv file.
* @param array $attemptids attempt no as in csv file => the id of the quiz_attempt as stored in the db.
*/
protected function check_attempts_results(array $results, array $attemptids) {
foreach ($results as $resultrow) {
$result = $this->explode_dot_separated_keys_to_make_subindexs($resultrow);
// Re-load quiz attempt data.
$attemptobj = quiz_attempt::create($attemptids[$result['quizattempt']]);
$this->check_attempt_results($result, $attemptobj);
}
}
/**
* Check that attempt results are as specified in $result.
*
* @param array $result row of data read from csv file.
* @param quiz_attempt $attemptobj the attempt object loaded from db.
*/
protected function check_attempt_results(array $result, quiz_attempt $attemptobj) {
foreach ($result as $fieldname => $value) {
if ($value === '!NULL!') {
$value = null;
}
switch ($fieldname) {
case 'quizattempt' :
break;
case 'attemptnumber' :
$this->assertEquals($value, $attemptobj->get_attempt_number());
break;
case 'slots' :
foreach ($value as $slotno => $slottests) {
foreach ($slottests as $slotfieldname => $slotvalue) {
switch ($slotfieldname) {
case 'mark' :
$this->assertEquals(round($slotvalue, 2), $attemptobj->get_question_mark($slotno),
"Mark for slot $slotno of attempt {$result['quizattempt']}.");
break;
default :
throw new \coding_exception('Unknown slots sub field column in csv file '
.s($slotfieldname));
}
}
}
break;
case 'finished' :
$this->assertEquals((bool)$value, $attemptobj->is_finished());
break;
case 'summarks' :
$this->assertEquals((float)$value, $attemptobj->get_sum_marks(),
"Sum of marks of attempt {$result['quizattempt']}.");
break;
case 'quizgrade' :
// Check quiz grades.
$grades = quiz_get_user_grades($attemptobj->get_quiz(), $attemptobj->get_userid());
$grade = array_shift($grades);
$this->assertEquals($value, $grade->rawgrade, "Quiz grade for attempt {$result['quizattempt']}.");
break;
case 'gradebookgrade' :
// Check grade book.
$gradebookgrades = grade_get_grades($attemptobj->get_courseid(),
'mod', 'quiz',
$attemptobj->get_quizid(),
$attemptobj->get_userid());
$gradebookitem = array_shift($gradebookgrades->items);
$gradebookgrade = array_shift($gradebookitem->grades);
$this->assertEquals($value, $gradebookgrade->grade, "Gradebook grade for attempt {$result['quizattempt']}.");
break;
default :
throw new \coding_exception('Unknown column in csv file '.s($fieldname));
}
}
}
}
+633
View File
@@ -0,0 +1,633 @@
<?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;
use moodle_url;
use question_bank;
use question_engine;
use mod_quiz\question\bank\qbank_helper;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Quiz attempt walk through.
*
* @package mod_quiz
* @category test
* @copyright 2013 The Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\quiz_attempt
*/
class attempt_walkthrough_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Create a quiz with questions and walk through a quiz attempt.
*/
public function test_quiz_attempt_walkthrough(): void {
global $SITE;
$this->resetAfterTest(true);
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 0, 'grade' => 100.0,
'sumgrades' => 3]);
// Create a couple of questions.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
$matchq = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
$description = $questiongenerator->create_question('description', null, ['category' => $cat->id]);
// Add them to the quiz.
quiz_add_quiz_question($saq->id, $quiz);
quiz_add_quiz_question($numq->id, $quiz);
quiz_add_quiz_question($matchq->id, $quiz);
quiz_add_quiz_question($description->id, $quiz);
// Make a user to do the quiz.
$user1 = $this->getDataGenerator()->create_user();
$quizobj = quiz_settings::create($quiz->id, $user1->id);
// Start the attempt.
$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, false, $timenow, false, $user1->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
$this->assertEquals('1,2,3,4,0', $attempt->layout);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Process some responses from the student.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
// The student has not answered any questions.
$this->assertEquals(3, $attemptobj->get_number_of_unanswered_questions());
$tosubmit = [1 => ['answer' => 'frog'],
2 => ['answer' => '3.14']];
$attemptobj->process_submitted_actions($timenow, false, $tosubmit);
// The student has answered two questions, and only one remaining.
$this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
$tosubmit = [
3 => [
'frog' => 'amphibian',
'cat' => 'mammal',
'newt' => ''
]
];
$attemptobj->process_submitted_actions($timenow, false, $tosubmit);
// The student has answered three questions but one is invalid, so there is still one remaining.
$this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
$tosubmit = [
3 => [
'frog' => 'amphibian',
'cat' => 'mammal',
'newt' => 'amphibian'
]
];
$attemptobj->process_submitted_actions($timenow, false, $tosubmit);
// The student has answered three questions, so there are no remaining.
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$attemptobj->process_finish($timenow, false);
// Re-load quiz attempt data.
$attemptobj = quiz_attempt::create($attempt->id);
// Check that results are stored as expected.
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertEquals(3, $attemptobj->get_sum_marks());
$this->assertEquals(true, $attemptobj->is_finished());
$this->assertEquals($timenow, $attemptobj->get_submitted_date());
$this->assertEquals($user1->id, $attemptobj->get_userid());
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
// Check quiz grades.
$grades = quiz_get_user_grades($quiz, $user1->id);
$grade = array_shift($grades);
$this->assertEquals(100.0, $grade->rawgrade);
// Check grade book.
$gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id);
$gradebookitem = array_shift($gradebookgrades->items);
$gradebookgrade = array_shift($gradebookitem->grades);
$this->assertEquals(100, $gradebookgrade->grade);
// Update question in quiz.
$newsa = $questiongenerator->update_question($saq, null,
['name' => 'This is the second version of shortanswer']);
$newnumbq = $questiongenerator->update_question($numq, null,
['name' => 'This is the second version of numerical']);
$newmatch = $questiongenerator->update_question($matchq, null,
['name' => 'This is the second version of match']);
$newdescription = $questiongenerator->update_question($description, null,
['name' => 'This is the second version of description']);
// Update the attempt to use this questions.
// Would not normally be done for a non-preview, but this is just a unit test.
$attemptobj->update_questions_to_new_version_if_changed();
// Verify.
$this->assertEquals($newsa->id, $attemptobj->get_question_attempt(1)->get_question_id());
$this->assertEquals($newnumbq->id, $attemptobj->get_question_attempt(2)->get_question_id());
$this->assertEquals($newmatch->id, $attemptobj->get_question_attempt(3)->get_question_id());
$this->assertEquals($newdescription->id, $attemptobj->get_question_attempt(4)->get_question_id());
// Repeat the checks from above.
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertEquals(3, $attemptobj->get_sum_marks());
$this->assertEquals(true, $attemptobj->is_finished());
$this->assertEquals($timenow, $attemptobj->get_submitted_date());
$this->assertEquals($user1->id, $attemptobj->get_userid());
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
// Re-load quiz attempt data and repeat the verification.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals($newsa->id, $attemptobj->get_question_attempt(1)->get_question_id());
$this->assertEquals($newnumbq->id, $attemptobj->get_question_attempt(2)->get_question_id());
$this->assertEquals($newmatch->id, $attemptobj->get_question_attempt(3)->get_question_id());
$this->assertEquals($newdescription->id, $attemptobj->get_question_attempt(4)->get_question_id());
// Repeat the checks from above.
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertEquals(3, $attemptobj->get_sum_marks());
$this->assertEquals(true, $attemptobj->is_finished());
$this->assertEquals($timenow, $attemptobj->get_submitted_date());
$this->assertEquals($user1->id, $attemptobj->get_userid());
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
}
/**
* Create a quiz containing one question and a close time.
*
* The question is the standard shortanswer test question.
* The quiz is set to close 1 hour from now.
* The quiz is set to use a grade period of 1 hour once time expires.
*
* @param string $overduehandling value for the overduehandling quiz setting.
* @return \stdClass the quiz that was created.
*/
protected function create_quiz_with_one_question(string $overduehandling = 'graceperiod'): \stdClass {
global $SITE;
$this->resetAfterTest();
// Make a quiz.
$timeclose = time() + HOURSECS;
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(
['course' => $SITE->id, 'timeclose' => $timeclose,
'overduehandling' => $overduehandling, 'graceperiod' => HOURSECS]);
// Create a question.
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
// Add them to the quiz.
$quizobj = quiz_settings::create($quiz->id);
quiz_add_quiz_question($saq->id, $quiz, 0, 1);
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
return $quiz;
}
public function test_quiz_attempt_walkthrough_submit_time_recorded_correctly_when_overdue(): void {
$quiz = $this->create_quiz_with_one_question();
// Make a user to do the quiz.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$quizobj = quiz_settings::create($quiz->id, $user->id);
// Start the attempt.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
// Process some responses from the student.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
$attemptobj->process_submitted_actions($quiz->timeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
// Attempt goes overdue (e.g. if cron ran).
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_going_overdue($quiz->timeclose + 2 * get_config('quiz', 'graceperiodmin'), false);
// Verify the attempt state.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertEquals(false, $attemptobj->is_finished());
$this->assertEquals(0, $attemptobj->get_submitted_date());
$this->assertEquals($user->id, $attemptobj->get_userid());
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
// Student submits the attempt during the grace period.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_attempt($quiz->timeclose + 30 * MINSECS, true, false, 1);
// Verify the attempt state.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertEquals(true, $attemptobj->is_finished());
$this->assertEquals($quiz->timeclose + 30 * MINSECS, $attemptobj->get_submitted_date());
$this->assertEquals($user->id, $attemptobj->get_userid());
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
}
public function test_quiz_attempt_walkthrough_close_time_extended_at_last_minute(): void {
global $DB;
$quiz = $this->create_quiz_with_one_question();
$originaltimeclose = $quiz->timeclose;
// Make a user to do the quiz.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$quizobj = quiz_settings::create($quiz->id, $user->id);
// Start the attempt.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
// Process some responses from the student during the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
// Teacher edits the quiz to extend the time-limit by one minute.
$DB->set_field('quiz', 'timeclose', $originaltimeclose + MINSECS, ['id' => $quiz->id]);
\course_modinfo::clear_instance_cache($quiz->course);
// Timer expires in the student browser and thinks it is time to submit the quiz.
// This sets $finishattempt to false - since the student did not click the button, and $timeup to true.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_attempt($originaltimeclose, false, true, 1);
// Verify the attempt state - the $timeup was ignored becuase things have changed server-side.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertFalse($attemptobj->is_finished());
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
$this->assertEquals(0, $attemptobj->get_submitted_date());
$this->assertEquals($user->id, $attemptobj->get_userid());
}
/**
* Create a quiz with a random as well as other questions and walk through quiz attempts.
*/
public function test_quiz_with_random_question_attempt_walkthrough(): void {
global $SITE;
$this->resetAfterTest(true);
question_bank::get_qtype('random')->clear_caches_before_testing();
$this->setAdminUser();
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 2, 'grade' => 100.0,
'sumgrades' => 4]);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Add two questions to question category.
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
// Add random question to the quiz.
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
// Make another category.
$cat2 = $questiongenerator->create_question_category();
$match = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
quiz_add_quiz_question($match->id, $quiz, 0);
$multichoicemulti = $questiongenerator->create_question('multichoice', 'two_of_four', ['category' => $cat->id]);
quiz_add_quiz_question($multichoicemulti->id, $quiz, 0);
$multichoicesingle = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]);
quiz_add_quiz_question($multichoicesingle->id, $quiz, 0);
foreach ([$saq->id => 'frog', $numq->id => '3.14'] as $randomqidtoselect => $randqanswer) {
// Make a new user to do the quiz each loop.
$user1 = $this->getDataGenerator()->create_user();
$this->setUser($user1);
$quizobj = quiz_settings::create($quiz->id, $user1->id);
// Start the attempt.
$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, false, $timenow);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, [1 => $randomqidtoselect]);
$this->assertEquals('1,2,0,3,4,0', $attempt->layout);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Process some responses from the student.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(4, $attemptobj->get_number_of_unanswered_questions());
$tosubmit = [];
$selectedquestionid = $quba->get_question_attempt(1)->get_question_id();
$tosubmit[1] = ['answer' => $randqanswer];
$tosubmit[2] = [
'frog' => 'amphibian',
'cat' => 'mammal',
'newt' => 'amphibian'];
$tosubmit[3] = ['One' => '1', 'Two' => '0', 'Three' => '1', 'Four' => '0']; // First and third choice.
$tosubmit[4] = ['answer' => 'One']; // The first choice.
$attemptobj->process_submitted_actions($timenow, false, $tosubmit);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
$attemptobj->process_finish($timenow, false);
// Re-load quiz attempt data.
$attemptobj = quiz_attempt::create($attempt->id);
// Check that results are stored as expected.
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertEquals(4, $attemptobj->get_sum_marks());
$this->assertEquals(true, $attemptobj->is_finished());
$this->assertEquals($timenow, $attemptobj->get_submitted_date());
$this->assertEquals($user1->id, $attemptobj->get_userid());
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
// Check quiz grades.
$grades = quiz_get_user_grades($quiz, $user1->id);
$grade = array_shift($grades);
$this->assertEquals(100.0, $grade->rawgrade);
// Check grade book.
$gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id);
$gradebookitem = array_shift($gradebookgrades->items);
$gradebookgrade = array_shift($gradebookitem->grades);
$this->assertEquals(100, $gradebookgrade->grade);
}
}
public function get_correct_response_for_variants() {
return [[1, 9.9], [2, 8.5], [5, 14.2], [10, 6.8, true]];
}
protected $quizwithvariants = null;
/**
* Create a quiz with a single question with variants and walk through quiz attempts.
*
* @dataProvider get_correct_response_for_variants
*/
public function test_quiz_with_question_with_variants_attempt_walkthrough($variantno, $correctresponse, $done = false): void {
global $SITE;
$this->resetAfterTest($done);
$this->setAdminUser();
if ($this->quizwithvariants === null) {
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$this->quizwithvariants = $quizgenerator->create_instance(['course' => $SITE->id,
'questionsperpage' => 0,
'grade' => 100.0,
'sumgrades' => 1]);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$calc = $questiongenerator->create_question('calculatedsimple', 'sumwithvariants', ['category' => $cat->id]);
quiz_add_quiz_question($calc->id, $this->quizwithvariants, 0);
}
// Make a new user to do the quiz.
$user1 = $this->getDataGenerator()->create_user();
$this->setUser($user1);
$quizobj = quiz_settings::create($this->quizwithvariants->id, $user1->id);
// Start the attempt.
$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, false, $timenow);
// Select variant.
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, [], [1 => $variantno]);
$this->assertEquals('1,0', $attempt->layout);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Process some responses from the student.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
$tosubmit = [1 => ['answer' => $correctresponse]];
$attemptobj->process_submitted_actions($timenow, false, $tosubmit);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
$attemptobj->process_finish($timenow, false);
// Re-load quiz attempt data.
$attemptobj = quiz_attempt::create($attempt->id);
// Check that results are stored as expected.
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertEquals(1, $attemptobj->get_sum_marks());
$this->assertEquals(true, $attemptobj->is_finished());
$this->assertEquals($timenow, $attemptobj->get_submitted_date());
$this->assertEquals($user1->id, $attemptobj->get_userid());
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
// Check quiz grades.
$grades = quiz_get_user_grades($this->quizwithvariants, $user1->id);
$grade = array_shift($grades);
$this->assertEquals(100.0, $grade->rawgrade);
// Check grade book.
$gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $this->quizwithvariants->id, $user1->id);
$gradebookitem = array_shift($gradebookgrades->items);
$gradebookgrade = array_shift($gradebookitem->grades);
$this->assertEquals(100, $gradebookgrade->grade);
}
public function test_quiz_attempt_walkthrough_abandoned_attempt_reopened_with_timelimit_override(): void {
global $DB;
$quiz = $this->create_quiz_with_one_question('autoabandon');
$originaltimeclose = $quiz->timeclose;
// Make a user to do the quiz.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$quizobj = quiz_settings::create($quiz->id, $user->id);
// Start the attempt.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
// Process some responses from the student during the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
// Student leaves, so cron closes the attempt when time expires.
$attemptobj->process_abandon($originaltimeclose + 5 * MINSECS, false);
// Verify the attempt state.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(quiz_attempt::ABANDONED, $attemptobj->get_state());
$this->assertEquals(0, $attemptobj->get_submitted_date());
$this->assertEquals($user->id, $attemptobj->get_userid());
// The teacher feels kind, so adds an override for the student, and re-opens the attempt.
$sink = $this->redirectEvents();
$overriddentimeclose = $originaltimeclose + HOURSECS;
$DB->insert_record('quiz_overrides', [
'quiz' => $quiz->id,
'userid' => $user->id,
'timeclose' => $overriddentimeclose,
]);
$attemptobj = quiz_attempt::create($attempt->id);
$reopentime = $originaltimeclose + 10 * MINSECS;
$attemptobj->process_reopen_abandoned($reopentime);
// Verify the attempt state.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertFalse($attemptobj->is_finished());
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
$this->assertEquals(0, $attemptobj->get_submitted_date());
$this->assertEquals($user->id, $attemptobj->get_userid());
$this->assertEquals($overriddentimeclose,
$attemptobj->get_access_manager($reopentime)->get_end_time($attemptobj->get_attempt()));
// Verify this was logged correctly.
$events = $sink->get_events();
$this->assertCount(1, $events);
$reopenedevent = array_shift($events);
$this->assertInstanceOf('\mod_quiz\event\attempt_reopened', $reopenedevent);
$this->assertEquals($attemptobj->get_context(), $reopenedevent->get_context());
$this->assertEquals(new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
$reopenedevent->get_url());
}
public function test_quiz_attempt_walkthrough_abandoned_attempt_reopened_after_close_time(): void {
$quiz = $this->create_quiz_with_one_question('autoabandon');
$originaltimeclose = $quiz->timeclose;
// Make a user to do the quiz.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$quizobj = quiz_settings::create($quiz->id, $user->id);
// Start the attempt.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
// Process some responses from the student during the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
// Student leaves, so cron closes the attempt when time expires.
$attemptobj->process_abandon($originaltimeclose + 5 * MINSECS, false);
// Verify the attempt state.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(quiz_attempt::ABANDONED, $attemptobj->get_state());
$this->assertEquals(0, $attemptobj->get_submitted_date());
$this->assertEquals($user->id, $attemptobj->get_userid());
// The teacher reopens the attempt without granting more time, so previously submitted responess are graded.
$sink = $this->redirectEvents();
$reopentime = $originaltimeclose + 10 * MINSECS;
$attemptobj->process_reopen_abandoned($reopentime);
// Verify the attempt state.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(1, $attemptobj->get_attempt_number());
$this->assertTrue($attemptobj->is_finished());
$this->assertEquals(quiz_attempt::FINISHED, $attemptobj->get_state());
$this->assertEquals($originaltimeclose, $attemptobj->get_submitted_date());
$this->assertEquals($user->id, $attemptobj->get_userid());
$this->assertEquals(1, $attemptobj->get_sum_marks());
// Verify this was logged correctly - there are some gradebook events between the two we want to check.
$events = $sink->get_events();
$this->assertGreaterThanOrEqual(2, $events);
$reopenedevent = array_shift($events);
$this->assertInstanceOf('\mod_quiz\event\attempt_reopened', $reopenedevent);
$this->assertEquals($attemptobj->get_context(), $reopenedevent->get_context());
$this->assertEquals(new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
$reopenedevent->get_url());
$submittedevent = array_pop($events);
$this->assertInstanceOf('\mod_quiz\event\attempt_submitted', $submittedevent);
$this->assertEquals($attemptobj->get_context(), $submittedevent->get_context());
$this->assertEquals(new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
$submittedevent->get_url());
}
}
+606
View File
@@ -0,0 +1,606 @@
<?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;
use core_question_generator;
use mod_quiz\task\update_overdue_attempts;
use mod_quiz_generator;
use question_engine;
use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/group/lib.php');
/**
* Unit tests for quiz attempt overdue handling
*
* @package mod_quiz
* @category test
* @copyright 2012 Matt Petro
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempts_test extends \advanced_testcase {
/**
* Test the functions quiz_update_open_attempts(), get_list_of_overdue_attempts() and
* update_overdue_attempts().
*/
public function test_bulk_update_functions(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Setup course, user and groups
$course = $this->getDataGenerator()->create_course();
$user1 = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->assertNotEmpty($studentrole);
$this->assertTrue(enrol_try_internal_enrol($course->id, $user1->id, $studentrole->id));
$group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$group3 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->assertTrue(groups_add_member($group1, $user1));
$this->assertTrue(groups_add_member($group2, $user1));
$usertimes = [];
/** @var mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
// Basic quiz settings
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1200, 'timelimit' => 600, 'message' => 'Test1A', 'time1000state' => 'finished'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 1800]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1200, 'timelimit' => 1800, 'message' => 'Test1B', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 0]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1200, 'timelimit' => 0, 'message' => 'Test1C', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 0, 'timelimit' => 600]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 600, 'message' => 'Test1D', 'time1000state' => 'finished'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 0, 'timelimit' => 0]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 0, 'message' => 'Test1E', 'time1000state' => 'inprogress'];
// Group overrides
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 0]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => null]);
$usertimes[$attemptid] = ['timeclose' => 1300, 'timelimit' => 0, 'message' => 'Test2A', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 0]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1100, 'timelimit' => null]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1100, 'timelimit' => 0, 'message' => 'Test2B', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(
['course' => $course->id, 'timeclose' => 0, 'timelimit' => 600, 'overduehandling' => 'autoabandon']);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => null, 'timelimit' => 700]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 700, 'message' => 'Test2C', 'time1000state' => 'abandoned'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 0, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => null, 'timelimit' => 500]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 500, 'message' => 'Test2D', 'time1000state' => 'finished'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 0, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => null, 'timelimit' => 0]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 0, 'message' => 'Test2E', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 500]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1300, 'timelimit' => 500, 'message' => 'Test2F', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1300, 'timelimit' => 500, 'message' => 'Test2G', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group3->id, 'timeclose' => 1300, 'timelimit' => 500]); // User not in group.
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1200, 'timelimit' => 600, 'message' => 'Test2H', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1200, 'timelimit' => 600, 'message' => 'Test2I', 'time1000state' => 'inprogress'];
// Multiple group overrides
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 501]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group2->id, 'timeclose' => 1301, 'timelimit' => 500]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 501, 'message' => 'Test3A', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 501, 'message' => 'Test3B', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1301, 'timelimit' => 500]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group2->id, 'timeclose' => 1300, 'timelimit' => 501]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 501, 'message' => 'Test3C', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 501, 'message' => 'Test3D', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600,
'overduehandling' => 'autoabandon']);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1301, 'timelimit' => 500]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group2->id, 'timeclose' => 1300, 'timelimit' => 501]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group3->id, 'timeclose' => 1500, 'timelimit' => 1000]); // User not in group.
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 501, 'message' => 'Test3E', 'time1000state' => 'abandoned'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 501, 'message' => 'Test3F', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 500]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group2->id, 'timeclose' => null, 'timelimit' => 501]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1300, 'timelimit' => 501, 'message' => 'Test3G', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1300, 'timelimit' => 501, 'message' => 'Test3H', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 500]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group2->id, 'timeclose' => 1301, 'timelimit' => null]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 500, 'message' => 'Test3I', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 500, 'message' => 'Test3J', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 500]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group2->id, 'timeclose' => 1301, 'timelimit' => 0]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 0, 'message' => 'Test3K', 'time1000state' => 'inprogress'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1301, 'timelimit' => 0, 'message' => 'Test3L', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 500]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group2->id, 'timeclose' => 0, 'timelimit' => 501]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 501, 'message' => 'Test3M', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 501, 'message' => 'Test3N', 'time1000state' => 'inprogress'];
// User overrides
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 700]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'userid' => $user1->id, 'timeclose' => 1201, 'timelimit' => 601]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1201, 'timelimit' => 601, 'message' => 'Test4A', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1201, 'timelimit' => 601, 'message' => 'Test4B', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 700]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'userid' => $user1->id, 'timeclose' => 0, 'timelimit' => 601]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 601, 'message' => 'Test4C', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 601, 'message' => 'Test4D', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 700]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'userid' => $user1->id, 'timeclose' => 1201, 'timelimit' => 0]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1201, 'timelimit' => 0, 'message' => 'Test4E', 'time1000state' => 'inprogress'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1201, 'timelimit' => 0, 'message' => 'Test4F', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600,
'overduehandling' => 'autoabandon']);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 700]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'userid' => $user1->id, 'timeclose' => null, 'timelimit' => 601]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1300, 'timelimit' => 601, 'message' => 'Test4G', 'time1000state' => 'abandoned'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1300, 'timelimit' => 601, 'message' => 'Test4H', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => null, 'timelimit' => 700]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'userid' => $user1->id, 'timeclose' => null, 'timelimit' => 601]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1200, 'timelimit' => 601, 'message' => 'Test4I', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1200, 'timelimit' => 601, 'message' => 'Test4J', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 700]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'userid' => $user1->id, 'timeclose' => 1201, 'timelimit' => null]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1201, 'timelimit' => 700, 'message' => 'Test4K', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1201, 'timelimit' => 700, 'message' => 'Test4L', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => null]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'userid' => $user1->id, 'timeclose' => 1201, 'timelimit' => null]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1201, 'timelimit' => 600, 'message' => 'Test4M', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1201, 'timelimit' => 600, 'message' => 'Test4N', 'time1000state' => 'inprogress'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => 700]);
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'userid' => 0, 'timeclose' => 1201, 'timelimit' => 601]); // Not user.
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1300, 'timelimit' => 700, 'message' => 'Test4O', 'time1000state' => 'finished'];
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 1000, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz), 'attempt' => 1]);
$usertimes[$attemptid] = ['timeclose' => 1300, 'timelimit' => 700, 'message' => 'Test4P', 'time1000state' => 'inprogress'];
// Attempt state overdue
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 600,
'overduehandling' => 'graceperiod', 'graceperiod' => 250]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'overdue',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 1200, 'timelimit' => 600, 'message' => 'Test5A', 'time1000state' => 'overdue'];
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 0, 'timelimit' => 600,
'overduehandling' => 'graceperiod', 'graceperiod' => 250]);
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'overdue',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
$usertimes[$attemptid] = ['timeclose' => 0, 'timelimit' => 600, 'message' => 'Test5B', 'time1000state' => 'overdue'];
// Compute expected end time for each attempt.
foreach ($usertimes as $attemptid => $times) {
$attempt = $DB->get_record('quiz_attempts', ['id' => $attemptid], '*', MUST_EXIST);
if ($times['timeclose'] > 0 && $times['timelimit'] > 0) {
$usertimes[$attemptid]['timedue'] = min($times['timeclose'], $attempt->timestart + $times['timelimit']);
} else if ($times['timeclose'] > 0) {
$usertimes[$attemptid]['timedue'] = $times['timeclose'];
} else if ($times['timelimit'] > 0) {
$usertimes[$attemptid]['timedue'] = $attempt->timestart + $times['timelimit'];
}
}
//
// Test quiz_update_open_attempts().
//
quiz_update_open_attempts(['courseid' => $course->id]);
foreach ($usertimes as $attemptid => $times) {
$attempt = $DB->get_record('quiz_attempts', ['id' => $attemptid], '*', MUST_EXIST);
if ($attempt->state == 'overdue') {
$graceperiod = $DB->get_field('quiz', 'graceperiod', ['id' => $attempt->quiz]);
} else {
$graceperiod = 0;
}
if (isset($times['timedue'])) {
$this->assertEquals($times['timedue'] + $graceperiod, $attempt->timecheckstate, $times['message']);
} else {
$this->assertNull($attempt->timecheckstate, $times['message']);
}
}
//
// Test get_list_of_overdue_attempts().
//
$overduehander = new update_overdue_attempts();
$attempts = $overduehander->get_list_of_overdue_attempts(100000); // way in the future
$count = 0;
foreach ($attempts as $attempt) {
$this->assertTrue(isset($usertimes[$attempt->id]));
$times = $usertimes[$attempt->id];
$this->assertEquals($times['timeclose'], $attempt->usertimeclose, $times['message']);
$this->assertEquals($times['timelimit'], $attempt->usertimelimit, $times['message']);
$count++;
}
$attempts->close();
$this->assertEquals($DB->count_records_select('quiz_attempts', 'timecheckstate IS NOT NULL'), $count);
$attempts = $overduehander->get_list_of_overdue_attempts(0); // before all attempts
$count = 0;
foreach ($attempts as $attempt) {
$count++;
}
$attempts->close();
$this->assertEquals(0, $count);
//
// Test update_overdue_attempts().
//
[$count, $quizcount] = $overduehander->update_all_overdue_attempts(1000, 940);
$attempts = $DB->get_records('quiz_attempts', null, 'quiz, userid, attempt',
'id, quiz, userid, attempt, state, timestart, timefinish, timecheckstate');
foreach ($attempts as $attempt) {
$this->assertTrue(isset($usertimes[$attempt->id]));
$times = $usertimes[$attempt->id];
$this->assertEquals($times['time1000state'], $attempt->state, $times['message']);
switch ($times['time1000state']) {
case 'finished':
$this->assertEquals($times['timedue'], $attempt->timefinish, $times['message']);
$this->assertNull($attempt->timecheckstate, $times['message']);
break;
case 'overdue':
$this->assertEquals(0, $attempt->timefinish, $times['message']);
$graceperiod = $DB->get_field('quiz', 'graceperiod', ['id' => $attempt->quiz]);
$this->assertEquals($times['timedue'] + $graceperiod, $attempt->timecheckstate, $times['message']);
break;
case 'abandoned':
$this->assertEquals(0, $attempt->timefinish, $times['message']);
$this->assertNull($attempt->timecheckstate, $times['message']);
break;
}
}
$this->assertEquals(19, $count);
$this->assertEquals(19, $quizcount);
}
/**
* Make any old question usage for a quiz.
*
* The attempts used in test_bulk_update_functions must have some
* question usage to store in uniqueid, but they don't have to be
* very realistic.
*
* @param \stdClass $quiz
* @return int question usage id.
*/
protected function usage_id(\stdClass $quiz): int {
$quba = question_engine::make_questions_usage_by_activity('mod_quiz',
\context_module::instance($quiz->cmid));
$quba->set_preferred_behaviour('deferredfeedback');
question_engine::save_questions_usage_by_activity($quba);
return $quba->get_id();
}
/**
* Test the group event handlers
*/
public function test_group_event_handlers(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Setup course, user and groups
$course = $this->getDataGenerator()->create_course();
$user1 = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->assertNotEmpty($studentrole);
$this->assertTrue(enrol_try_internal_enrol($course->id, $user1->id, $studentrole->id));
$group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->assertTrue(groups_add_member($group1, $user1));
$this->assertTrue(groups_add_member($group2, $user1));
/** @var mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => 1200, 'timelimit' => 0]);
// add a group1 override
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group1->id, 'timeclose' => 1300, 'timelimit' => null]);
// add an attempt
$attemptid = $DB->insert_record('quiz_attempts', ['quiz' => $quiz->id, 'userid' => $user1->id, 'state' => 'inprogress',
'timestart' => 100, 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => $this->usage_id($quiz)]);
// update timecheckstate
quiz_update_open_attempts(['quizid' => $quiz->id]);
$this->assertEquals(1300, $DB->get_field('quiz_attempts', 'timecheckstate', ['id' => $attemptid]));
// remove from group
$this->assertTrue(groups_remove_member($group1, $user1));
$this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', ['id' => $attemptid]));
// add back to group
$this->assertTrue(groups_add_member($group1, $user1));
$this->assertEquals(1300, $DB->get_field('quiz_attempts', 'timecheckstate', ['id' => $attemptid]));
// delete group
groups_delete_group($group1);
$this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', ['id' => $attemptid]));
$this->assertEquals(0, $DB->count_records('quiz_overrides', ['quiz' => $quiz->id]));
// add a group2 override
$DB->insert_record('quiz_overrides',
['quiz' => $quiz->id, 'groupid' => $group2->id, 'timeclose' => 1400, 'timelimit' => null]);
quiz_update_open_attempts(['quizid' => $quiz->id]);
$this->assertEquals(1400, $DB->get_field('quiz_attempts', 'timecheckstate', ['id' => $attemptid]));
// delete user1 from all groups
groups_delete_group_members($course->id, $user1->id);
$this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', ['id' => $attemptid]));
// add back to group2
$this->assertTrue(groups_add_member($group2, $user1));
$this->assertEquals(1400, $DB->get_field('quiz_attempts', 'timecheckstate', ['id' => $attemptid]));
// delete everyone from all groups
groups_delete_group_members($course->id);
$this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', ['id' => $attemptid]));
}
/**
* Test the functions quiz_create_attempt_handling_errors
*/
public function test_quiz_create_attempt_handling_errors(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
// Make a quiz.
$course = $this->getDataGenerator()->create_course();
$user1 = $this->getDataGenerator()->create_user();
$student = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
/** @var mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'questionsperpage' => 0, 'grade' => 100.0,
'sumgrades' => 2]);
// Create questions.
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
// Add them to the quiz.
quiz_add_quiz_question($saq->id, $quiz);
quiz_add_quiz_question($numq->id, $quiz);
$quizobj = quiz_settings::create($quiz->id, $user1->id);
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$timenow = time();
// Create an attempt.
$attempt = quiz_create_attempt($quizobj, 1, null, $timenow, false, $user1->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
$result = quiz_create_attempt_handling_errors($attempt->id, $quiz->cmid);
$this->assertEquals($result->get_attemptid(), $attempt->id);
try {
$result = quiz_create_attempt_handling_errors($attempt->id, 9999);
$this->fail('Exception expected due to invalid course module id.');
} catch (\moodle_exception $e) {
$this->assertEquals('invalidcoursemodule', $e->errorcode);
}
try {
quiz_create_attempt_handling_errors(9999, $result->get_cmid());
$this->fail('Exception expected due to quiz content change.');
} catch (\moodle_exception $e) {
$this->assertEquals('attempterrorcontentchange', $e->errorcode);
}
try {
quiz_create_attempt_handling_errors(9999);
$this->fail('Exception expected due to invalid quiz attempt id.');
} catch (\moodle_exception $e) {
$this->assertEquals('attempterrorinvalid', $e->errorcode);
}
// Set up as normal user without permission to view preview.
$this->setUser($student->id);
try {
quiz_create_attempt_handling_errors(9999, $result->get_cmid());
$this->fail('Exception expected due to quiz content change for user without permission.');
} catch (\moodle_exception $e) {
$this->assertEquals('attempterrorcontentchangeforuser', $e->errorcode);
}
try {
quiz_create_attempt_handling_errors($attempt->id, 9999);
$this->fail('Exception expected due to invalid course module id for user without permission.');
} catch (\moodle_exception $e) {
$this->assertEquals('invalidcoursemodule', $e->errorcode);
}
}
}
@@ -0,0 +1,155 @@
<?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\backup;
use advanced_testcase;
use backup_controller;
use restore_controller;
use quiz_question_helper_test_trait;
use backup;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
require_once($CFG->dirroot . '/question/engine/lib.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Test repeatedly restoring a quiz into another course.
*
* @package mod_quiz
* @category test
* @copyright Julien Rädler
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \restore_questions_parser_processor
* @covers \restore_create_categories_and_questions
*/
final class repeated_restore_test extends advanced_testcase {
use quiz_question_helper_test_trait;
/**
* Restore a quiz twice into the same target course, and verify the quiz uses the restored questions both times.
*/
public function test_restore_quiz_into_other_course_twice(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
// Step 1: Create two courses and a user with editing teacher capabilities.
$generator = $this->getDataGenerator();
$course1 = $generator->create_course();
$course2 = $generator->create_course();
$teacher = $USER;
$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
$generator->enrol_user($teacher->id, $course2->id, 'editingteacher');
// Create a quiz with questions in the first course.
$quiz = $this->create_test_quiz($course1);
$coursecontext = \context_course::instance($course1->id);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Create a question category.
$cat = $questiongenerator->create_question_category(['contextid' => $coursecontext->id]);
// Create a short answer question.
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
// Update the question to simulate editing.
$questiongenerator->update_question($saq);
// Add question to quiz.
quiz_add_quiz_question($saq->id, $quiz);
// Create a numerical question.
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
// Update the question to simulate multiple versions.
$questiongenerator->update_question($numq);
$questiongenerator->update_question($numq);
// Add question to quiz.
quiz_add_quiz_question($numq->id, $quiz);
// Create a true false question.
$tfq = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
// Update the question to simulate multiple versions.
$questiongenerator->update_question($tfq);
$questiongenerator->update_question($tfq);
// Add question to quiz.
quiz_add_quiz_question($tfq->id, $quiz);
// Capture original question IDs for verification after import.
$modules1 = get_fast_modinfo($course1->id)->get_instances_of('quiz');
$module1 = reset($modules1);
$questionscourse1 = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module1->instance, $module1->context);
$originalquestionids = [];
foreach ($questionscourse1 as $slot) {
array_push($originalquestionids, intval($slot->questionid));
}
// Step 2: Backup the first course.
$bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
// Step 3: Import the backup into the second course.
$rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
$teacher->id, backup::TARGET_CURRENT_ADDING);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify the question ids from the quiz in the original course are different
// from the question ids in the duplicated quiz in the second course.
$modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module2 = reset($modules2);
$questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module2->instance, $module2->context);
foreach ($questionscourse2firstimport as $slot) {
$this->assertNotContains(intval($slot->questionid), $originalquestionids,
"Question ID $slot->questionid should not be in the original course's question IDs.");
}
// Repeat the backup and import process to simulate a second import.
$bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
$rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
$teacher->id, backup::TARGET_CURRENT_ADDING);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify that the second restore has used the same new questions that were created by the first restore.
$modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module3 = end($modules3);
$questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module3->instance, $module3->context);
foreach ($questionscourse2secondimport as $slot) {
$this->assertEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid);
}
}
}
+83
View File
@@ -0,0 +1,83 @@
<?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\backup;
use advanced_testcase;
use backup;
use restore_controller;
/**
* Test restoring 3.9 backups including random questions.
*
* @package mod_quiz
* @copyright 2024 Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \restore_move_module_questions_categories
*/
final class restore_39_test extends advanced_testcase {
public function test_restore_random_question_39(): void {
global $DB, $CFG, $USER;
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
$this->resetAfterTest();
$this->setAdminUser();
// The example Moodle 3.9 backup file used in this test is an activity-level backup of a quiz.
// So, the backup contains just the quiz-level question bank which contains:
// | - Question category: Top
// | - Question category: Default for Test MDL-78902 quiz
// | - Question: Test MDL-78902 T/F question
// | - Question: Random (Default for Test MDL-78902 quiz)
// The quiz itself contains 1 question, the random question.
// So, during the restore, the quiz_slot needs to be updated to use a question_set_reference.
$backupfile = 'moodle_39_quiz_with_random_question_from_mod_context';
// Extract backup file.
$backupid = $backupfile;
$backuppath = make_backup_temp_directory($backupid);
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
__DIR__ . "/../fixtures/$backupfile.mbz", $backuppath);
// Restore the quiz activity in the backup from Moodle 3.9 to a new course.
$coursecat = self::getDataGenerator()->create_category();
$course = self::getDataGenerator()->create_course(['category' => $coursecat->id]);
$rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_NO,
backup::MODE_GENERAL, $USER->id, backup::TARGET_EXISTING_ADDING);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
// Get information about the quiz activity and confirm the references are correct.
$modinfo = get_fast_modinfo($course->id);
$quizzes = array_values($modinfo->get_instances_of('quiz'));
// Get contextid for the restored quiz activity.
$contextid = $quizzes[0]->context->id;
$qcats = $DB->get_records('question_categories', ['contextid' => $contextid], 'parent');
// Confirm there are 2 question categories for the restored quiz activity.
$this->assertEquals(['top', 'Default for Test MDL-78902 quiz'], array_column($qcats, 'name'));
// Get question_set_references records for the restored quiz activity.
$references = $DB->get_records('question_set_references', ['usingcontextid' => $contextid]);
foreach ($references as $reference) {
$filtercondition = json_decode($reference->filtercondition);
// Confirm the questionscontextid is set correctly, which is from filter question category id.
$this->assertEquals($reference->questionscontextid,
$qcats[$filtercondition->questioncategoryid]->contextid);
}
}
}
+109
View File
@@ -0,0 +1,109 @@
<?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\backup;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
/**
* Restore date tests.
*
* @package mod_quiz
* @copyright 2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_date_test extends \restore_date_testcase {
/**
* Test restore dates.
*/
public function test_restore_dates(): void {
global $DB, $USER;
// Create quiz data.
$record = ['timeopen' => 100, 'timeclose' => 100, 'timemodified' => 100, 'tiemcreated' => 100, 'questionsperpage' => 0,
'grade' => 100.0, 'sumgrades' => 2];
list($course, $quiz) = $this->create_course_and_module('quiz', $record);
// Create questions.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
// Add to the quiz.
quiz_add_quiz_question($saq->id, $quiz);
// Create an attempt.
$timestamp = 100;
$quizobj = \mod_quiz\quiz_settings::create($quiz->id);
$attempt = quiz_create_attempt($quizobj, 1, false, $timestamp, false);
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timestamp);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Quiz grade.
$grade = new \stdClass();
$grade->quiz = $quiz->id;
$grade->userid = $USER->id;
$grade->grade = 8.9;
$grade->timemodified = $timestamp;
$grade->id = $DB->insert_record('quiz_grades', $grade);
// User override.
$override = (object)[
'quiz' => $quiz->id,
'groupid' => 0,
'userid' => $USER->id,
'sortorder' => 1,
'timeopen' => 100,
'timeclose' => 200
];
$DB->insert_record('quiz_overrides', $override);
// Set time fields to a constant for easy validation.
$DB->set_field('quiz_attempts', 'timefinish', $timestamp);
// Do backup and restore.
$newcourseid = $this->backup_and_restore($course);
$newquiz = $DB->get_record('quiz', ['course' => $newcourseid]);
$this->assertFieldsNotRolledForward($quiz, $newquiz, ['timecreated', 'timemodified']);
$props = ['timeclose', 'timeopen'];
$this->assertFieldsRolledForward($quiz, $newquiz, $props);
$newattempt = $DB->get_record('quiz_attempts', ['quiz' => $newquiz->id]);
$newoverride = $DB->get_record('quiz_overrides', ['quiz' => $newquiz->id]);
$newgrade = $DB->get_record('quiz_grades', ['quiz' => $newquiz->id]);
// Attempt time checks.
$diff = $this->get_diff();
$this->assertEquals($timestamp, $newattempt->timemodified);
$this->assertEquals($timestamp, $newattempt->timefinish);
$this->assertEquals($timestamp, $newattempt->timestart);
$this->assertEquals($timestamp + $diff, $newattempt->timecheckstate); // Should this be rolled?
// Quiz override time checks.
$diff = $this->get_diff();
$this->assertEquals($override->timeopen + $diff, $newoverride->timeopen);
$this->assertEquals($override->timeclose + $diff, $newoverride->timeclose);
// Quiz grade time checks.
$this->assertEquals($grade->timemodified, $newgrade->timemodified);
}
}
@@ -0,0 +1,90 @@
<?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;
use core_question_generator;
use mod_quiz_generator;
use restore_date_testcase;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
/**
* Test of backup and restore of quiz grade items.
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \backup_quiz_activity_structure_step
* @covers \restore_quiz_activity_structure_step
*/
final class restore_quiz_grade_items_test extends restore_date_testcase {
public function test_restore_quiz_grade_items(): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
/** @var mod_quiz_generator $quizgen */
$quizgen = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Create a quiz with two grade items.
$course = $generator->create_course();
$quiz = $quizgen->create_instance(['course' => $course->id]);
$listeninggrade = $quizgen->create_grade_item(['quizid' => $quiz->id, 'name' => 'Listening']);
$readinggrade = $quizgen->create_grade_item(['quizid' => $quiz->id, 'name' => 'Reading']);
// Add two questions to the quiz.
$cat = $questiongenerator->create_question_category();
$saq1 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$saq2 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($saq1->id, $quiz, 0, 1);
quiz_add_quiz_question($saq2->id, $quiz, 0, 1);
// Set one of the question to use a grade item.
$quizobj = quiz_settings::create($quiz->id);
$structure = $quizobj->get_structure();
$structure->update_slot_grade_item($structure->get_slot_by_number(2), $readinggrade->id);
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
// Back up and restore the course.
$newcourseid = $this->backup_and_restore($course);
// Verify the grade items were copied over.
$newquiz = $DB->get_record('quiz', ['course' => $newcourseid]);
$quizobj = quiz_settings::create($newquiz->id);
$structure = $quizobj->get_structure();
$quizgradeitems = array_values($structure->get_grade_items());
// Check the grade items are right in the restored quiz.
$this->assertEquals(
[
(object) ['id' => reset($quizgradeitems)->id, 'quizid' => $newquiz->id, 'sortorder' => 1, 'name' => 'Listening'],
(object) ['id' => end($quizgradeitems)->id, 'quizid' => $newquiz->id, 'sortorder' => 2, 'name' => 'Reading'],
],
array_values($quizgradeitems),
);
// Verify that each slot uses the right grade item.
$this->assertNull($structure->get_slot_by_number(1)->quizgradeitemid);
$this->assertEquals(end($quizgradeitems)->id, $structure->get_slot_by_number(2)->quizgradeitemid);
}
}
+61
View File
@@ -0,0 +1,61 @@
@mod @mod_quiz
Feature: Add a quiz
In order to evaluate students
As a teacher
I need to create a quiz
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Terry1 | Teacher1 | teacher1@example.com |
| student1 | Sam1 | Student1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activity" exists:
| activity | quiz |
| course | C1 |
| idnumber | 00001 |
| name | Test quiz name |
| intro | Test quiz description |
| section | 1 |
| grade | 10 |
When I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "True/False" question to the "Test quiz name" quiz with:
| Question name | First question |
| Question text | Answer the first question |
| General feedback | Thank you, this is the general feedback |
| Correct answer | False |
| Feedback for the response 'True'. | So you think it is true |
| Feedback for the response 'False'. | So you think it is false |
And I log out
And I am on the "Test quiz name" "quiz activity" page logged in as student1
And I press "Attempt quiz"
Then I should see "Question 1"
And I should see "Answer the first question"
And I set the field "True" to "1"
And I press "Finish attempt ..."
And I should see "Answer saved"
And I press "Submit all and finish"
@javascript @skip_chrome_zerosize
Scenario: Add and configure small quiz and perform an attempt as a student with Javascript enabled
Then I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue"
And I should see "So you think it is true"
And I should see "Thank you, this is the general feedback"
And I should see "The correct answer is 'False'."
And I follow "Finish review"
And I should see "Highest grade: 0.00 / 10.00."
Scenario: Add and configure small quiz and perform an attempt as a student with Javascript disabled
Then I should see "So you think it is true"
And I should see "Thank you, this is the general feedback"
And I should see "The correct answer is 'False'."
And I follow "Finish review"
And I should see "Highest grade: 0.00 / 10.00."
+236
View File
@@ -0,0 +1,236 @@
@mod @mod_quiz
Feature: Attempt a quiz
As a student
In order to demonstrate what I know
I need to be able to attempt quizzes
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student | Student | One | student@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
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 "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
| Test questions | truefalse | TF3 | Third question |
| Test questions | truefalse | TF4 | Fourth question |
| Test questions | truefalse | TF5 | Fifth question |
| Test questions | truefalse | TF6 | Sixth question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | grade | navmethod |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 100 | free |
| quiz | Quiz 2 | Quiz 2 description | C1 | quiz2 | 6 | free |
| quiz | Quiz 3 | Quiz 3 description | C1 | quiz3 | 100 | free |
| quiz | Quiz 4 | Quiz 4 description | C1 | quiz4 | 100 | sequential |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | |
| TF2 | 1 | 3.0 |
And quiz "Quiz 2" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 1 |
| TF3 | 2 |
| TF4 | 3 |
| TF5 | 4 |
| TF6 | 4 |
And quiz "Quiz 2" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 0 |
| Section 2 | 3 | 0 |
| | 4 | 1 |
| Section 3 | 5 | 1 |
And quiz "Quiz 3" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
And quiz "Quiz 4" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
@javascript
Scenario: Attempt a quiz with a single unnamed section, review and re-attempt
Given user "student" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | False |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I follow "Review"
And I should see "Status"
Then I should see "Started"
And I should see "Completed"
And I should see "Duration"
And I should see "Marks"
And I should see "Grade"
And I should see "25.00 out of 100.00"
And I follow "Finish review"
And I press "Re-attempt quiz"
@javascript
Scenario: Attempt a quiz with multiple sections
Given I am on the "Quiz 2" "mod_quiz > View" page logged in as "student"
When I press "Attempt quiz"
Then I should see "Section 1" in the "Quiz navigation" "block"
And I should see question "1" in section "Section 1" in the quiz navigation
And I should see question "2" in section "Section 1" in the quiz navigation
And I should see question "3" in section "Section 2" in the quiz navigation
And I should see question "4" in section "Untitled section" in the quiz navigation
And I should see question "5" in section "Section 3" in the quiz navigation
And I should see question "6" in section "Section 3" in the quiz navigation
And I click on "True" "radio" in the "First question" "question"
And I follow "Finish attempt ..."
And I should see question "1" in section "Section 1" in the quiz navigation
And I should see question "2" in section "Section 1" in the quiz navigation
And I should see question "3" in section "Section 2" in the quiz navigation
And I should see question "4" in section "Untitled section" in the quiz navigation
And I should see question "5" in section "Section 3" in the quiz navigation
And I should see question "6" in section "Section 3" in the quiz navigation
And I should see "Section 1" in the "quizsummaryofattempt" "table"
And I should see "Section 2" in the "quizsummaryofattempt" "table"
And I should see "Untitled section" in the "quizsummaryofattempt" "table"
And I should see "Section 3" in the "quizsummaryofattempt" "table"
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"
And I should see "1.00 out of 6.00 (16.67%)" in the "Grade" "table_row"
And I should see question "1" in section "Section 1" in the quiz navigation
And I should see question "2" in section "Section 1" in the quiz navigation
And I should see question "3" in section "Section 2" in the quiz navigation
And I should see question "4" in section "Untitled section" in the quiz navigation
And I should see question "5" in section "Section 3" in the quiz navigation
And I should see question "6" in section "Section 3" in the quiz navigation
And I follow "Show one page at a time"
And I should see "First question"
And I should not see "Third question"
And I should see "Next page"
And I follow "Show all questions on one page"
And I should see "Fourth question"
And I should see "Sixth question"
And I should not see "Next page"
@javascript
Scenario: Next and previous navigation
Given I am on the "Quiz 3" "mod_quiz > View" page logged in as "student"
When I press "Attempt quiz"
Then I should see "First question"
And I should not see "Second question"
And I press "Next page"
And I should see "Second question"
And I should not see "First question"
And I click on "Finish attempt ..." "button" in the "region-main" "region"
And I should see "Summary of attempt"
And I press "Return to attempt"
And I should see "Second question"
And I should not see "First question"
And I press "Previous page"
And I should see "First question"
And I should not see "Second question"
And I follow "Finish attempt ..."
And I press "Submit all and finish"
And I should see "Once you submit your answers, you wont be able to change them." in the "Submit all your answers and finish?" "dialogue"
And I should see "Questions without a response: 2" in the "Submit all your answers and finish?" "dialogue"
And I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue"
And I should see "0.00 out of 100.00" in the "Grade" "table_row"
And I should see "First question"
And I should see "Second question"
And I follow "Show one page at a time"
And I should see "0.00 out of 100.00" in the "Grade" "table_row"
And I should see "First question"
And I should not see "Second question"
And I follow "Next page"
And "Grade" "table_row" should not exist
And I should see "Second question"
And I should not see "First question"
And I follow "Previous page"
And I should see "0.00 out of 100.00" in the "Grade" "table_row"
And I should see "First question"
And I should not see "Second question"
@javascript
Scenario: Next and previous with sequential navigation method
Given I am on the "Quiz 4" "mod_quiz > View" page logged in as "student"
When I press "Attempt quiz"
Then I should see "First question"
And I should not see "Second question"
And I press "Next page"
And I should see "Second question"
And I should not see "First question"
And "Previous page" "button" should not exist
And I click on "Finish attempt ..." "button" in the "region-main" "region"
And I should see "Summary of attempt"
And I press "Submit all and finish"
And I should see "Once you submit your answers, you wont be able to change them." in the "Submit all your answers and finish?" "dialogue"
And I should not see "Questions without a response: 2" in the "Submit all your answers and finish?" "dialogue"
And I click on "Submit" "button" in the "Submit all your answers and finish?" "dialogue"
And I should see "First question"
And I should see "Second question"
And I follow "Show one page at a time"
And I should see "First question"
And I should not see "Second question"
And I follow "Next page"
And I should see "Second question"
And I should not see "First question"
And I follow "Previous page"
And I should see "First question"
And I should not see "Second question"
@javascript
Scenario: Take a quiz with number of attempts set
Given the following "activities" exist:
| activity | name | course | grade | navmethod | attempts |
| quiz | Quiz 5 | C1 | 100 | free | 2 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF7 | First question |
And quiz "Quiz 5" contains the following questions:
| question | page |
| TF7 | 1 |
And user "student" has attempted "Quiz 5" with responses:
| slot | response |
| 1 | True |
When I am on the "Quiz 5" "mod_quiz > View" page logged in as "student"
Then I should see "Attempts allowed: 2"
And I should not see "No more attempts are allowed"
And I press "Re-attempt quiz"
And I should see "First question"
And I click on "Finish attempt ..." "button" in the "region-main" "region"
And I press "Submit all and finish"
And I should see "Once you submit your answers, you wont be able to change them." in the "Submit all your answers and finish?" "dialogue"
And I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue"
And I follow "Finish review"
And I should not see "Re-attempt quiz"
And I should see "No more attempts are allowed"
@javascript
Scenario: Student still sees the same version after the question is edited.
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I should see "First question"
And I click on "False" "radio" in the "First question" "question"
And I press "Finish attempt ..."
And I log out
And I am on the "Quiz 1" "mod_quiz > View" page logged in as "admin"
And I press "Preview quiz"
And I click on "Edit question" "link" in the "First question" "question"
And I set the field "Question text" to "First question version 2"
And I press "id_submitbutton"
And I should see "v2 (latest)" in the "First question" "question"
And I log out
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Continue your attempt"
Then I should see "First question"
And I should not see "First question version 2"
+106
View File
@@ -0,0 +1,106 @@
@mod @mod_quiz
Feature: The various checks that may happen when an attept is started
As a student
In order to start a quiz with confidence
I need to be waned if there is a time limit, or various similar things
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student | Student | One | student@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
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 "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | Text of the first question |
@javascript
Scenario: Start a quiz with no time limit
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
Then I should see "Text of the first question"
And I should not see "v1" in the "Question 1" "question"
@javascript
Scenario: Start a quiz with time limit and password
Given the following "activities" exist:
| activity | name | intro | course | idnumber | timelimit | quizpassword |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 3600 | Frog |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
Then I should see "To attempt this quiz you need to know the quiz password" in the "Start attempt" "dialogue"
And I should see "Your attempt will have a time limit of 1 hour. When you " in the "Start attempt" "dialogue"
And I set the field "Quiz password" to "Frog"
And I click on "Start attempt" "button" in the "Start attempt" "dialogue"
And I should see "Text of the first question"
@javascript
Scenario: Cancel starting a quiz with time limit and password
Given the following "activities" exist:
| activity | name | intro | course | idnumber | timelimit | quizpassword |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 3600 | Frog |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I click on "Cancel" "button" in the "Start attempt" "dialogue"
Then I should see "Quiz 1 description"
And "Attempt quiz" "button" should be visible
@javascript
Scenario: Start a quiz with time limit and password, get the password wrong first time
Given the following "activities" exist:
| activity | name | intro | course | idnumber | timelimit | quizpassword |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 3600 | Frog |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I set the field "Quiz password" to "Toad"
And I click on "Start attempt" "button" in the "Start attempt" "dialogue"
Then I should see "Quiz 1 description"
And I should see "To attempt this quiz you need to know the quiz password"
And I should see "Your attempt will have a time limit of 1 hour. When you "
And I should see "The password entered was incorrect"
And I set the field "Quiz password" to "Frog"
And I press "Start attempt"
And I should see "Text of the first question"
@javascript
Scenario: Start a quiz with time limit and password, get the password wrong first time then cancel
Given the following "activities" exist:
| activity | name | intro | course | idnumber | timelimit | quizpassword |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 3600 | Frog |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I set the field "Quiz password" to "Toad"
And I click on "Start attempt" "button" in the "Start attempt" "dialogue"
And I should see "Quiz 1 description"
And I should see "To attempt this quiz you need to know the quiz password"
And I should see "Your attempt will have a time limit of 1 hour. When you "
And I should see "The password entered was incorrect"
And I set the field "Quiz password" to "Frog"
And I press "Cancel"
Then I should see "Quiz 1 description"
And "Attempt quiz" "button" should be visible
@@ -0,0 +1,57 @@
@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"
And I follow "Finish review"
And I should not see "Unused grade item"
And I should see "1.00/2.00" in the "Marks" "table_row"
And I should see "Grade" in the "50.00 out of 100.00" "table_row"
@@ -0,0 +1,228 @@
@mod @mod_quiz
Feature: Allow students to redo questions in a practice quiz, without starting a whole new attempt
In order to practice particular skills I am struggling with
As a student
I need to be able to redo each question in a quiz as often as necessary without starting a whole new attempt, if my teacher allows it.
Background:
Given the following "users" exist:
| username | firstname | lastname |
| student | Student | One |
| teacher | Teacher | One |
| editor | Question | Editor |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student | C1 | student |
| teacher | C1 | teacher |
| editor | 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 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | canredoquestions |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | 1 |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | 2 |
| TF2 | 1 | 1 |
@javascript
Scenario: After completing a question, there is a redo question button that restarts the question
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
When I press "Attempt quiz"
And I click on "False" "radio" in the "First question" "question"
And I click on "Check" "button" in the "First question" "question"
And I press "Try another question like this one"
Then the state of "First question" question is shown as "Not complete"
And I should see "Marked out of 2.00" in the "First question" "question"
@javascript
Scenario: After redoing a question, regrade works
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
When I press "Attempt quiz"
And I click on "False" "radio" in the "First question" "question"
And I click on "Check" "button" in the "First question" "question"
And I press "Try another question like this one"
And I am on the "Quiz 1" "mod_quiz > Grades report" page logged in as "teacher"
And I press "Regrade all"
Then I should see "Finished regrading (1/1)"
And I should see "Regrade completed"
And I press "Continue"
# Regrade a second time, to ensure the first regrade did not corrupt any data.
And I press "Regrade all"
And I should see "Finished regrading (1/1)"
And I should see "Regrade completed"
@javascript
Scenario: Start attempt, teacher edits question, redo picks up latest non-draft version
# Start attempt as student.
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I click on "False" "radio" in the "First question" "question"
And I click on "Check" "button" in the "First question" "question"
And I log out
# Now edit the question as teacher to add a real version and a draft version.
# Would be nice to do this with a generator, but I don't have time right now.
And I am on the "TF1" "core_question > edit" page logged in as "editor"
And I set the following fields to these values:
| Question name | TF1-v2 |
| Question text | The new first question |
| Correct answer | False |
And I press "id_submitbutton"
And I am on the "TF1-v2" "core_question > edit" page
And I set the following fields to these values:
| Question name | TF1-v3 |
| Question text | This is only draft for now |
| Correct answer | True |
| Question status | Draft |
And I press "id_submitbutton"
And I log out
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Continue your attempt"
And I press "Try another question like this one"
Then the state of "The new first question" question is shown as "Not complete"
And I should see "Marked out of 2.00" in the "The new first question" "question"
And I should not see "This is only draft for now"
@javascript
Scenario: The redo question button is visible but disabled for teachers
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
When I press "Attempt quiz"
And I click on "False" "radio" in the "First question" "question"
And I click on "Check" "button" in the "First question" "question"
And I log out
And I am on the "Quiz 1" "mod_quiz > View" page logged in as "teacher"
And I follow "Attempts: 1"
And I follow "Review attempt"
Then the "Try another question like this one" "button" should be disabled
@javascript
Scenario: The redo question buttons are no longer visible after the attempt is submitted.
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
When I press "Attempt quiz"
And I click on "False" "radio" in the "First question" "question"
And I click on "Check" "button" in the "First 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"
Then "Try another question like this one" "button" should not exist
@javascript @_switch_window
Scenario: Teachers reviewing can see all the questions attempted in a slot
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I click on "False" "radio" in the "First question" "question"
And I click on "Check" "button" in the "First question" "question"
And I press "Try another question like this one"
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"
And I log out
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "teacher"
And I follow "Attempts: 1"
And I follow "Review attempt"
And I click on "1" "link" in the "First question" "question"
And I switch to "reviewquestion" window
Then the state of "First question" question is shown as "Incorrect"
And I click on "1" "link" in the "First question" "question"
And the state of "First question" question is shown as "Not complete"
And I switch to the main window
And the state of "First question" question is shown as "Not answered"
And I should not see "Submit" in the ".history" "css_element"
And I am on the "Quiz 1" "mod_quiz > Statistics report" page logged in as teacher
And I follow "TF1"
And "False" row "Frequency" column of "quizresponseanalysis" table should contain "100.00%"
And "True" row "Frequency" column of "quizresponseanalysis" table should contain "0.00%"
And "[No response]" row "Frequency" column of "quizresponseanalysis" table should contain "100.00%"
@javascript @_switch_window
Scenario: Teachers reviewing can switch between attempts in the review question popup
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as student
# Create two attempts, only one of which has a redo.
When I press "Attempt quiz"
And I click on "False" "radio" in the "First question" "question"
And I click on "Check" "button" in the "First question" "question"
And I press "Try another question like this one"
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"
And I follow "Finish review"
And I press "Re-attempt quiz"
And I click on "True" "radio" in the "First question" "question"
And I click on "Check" "button" in the "First question" "question"
And I log out
And I am on the "Quiz 1" "mod_quiz > View" page logged in as teacher
And I follow "Attempts: 2"
# Review the first attempt - and switch to the first question seen.
And I follow "Review attempt"
And I click on "1" "link" in the "First question" "question"
And I switch to "reviewquestion" window
And the state of "First question" question is shown as "Incorrect"
# Now switch to the other quiz attempt using the link at the top, which does not have a redo.
And I click on "2" "link" in the "Attempts" "table_row"
Then the state of "First question" question is shown as "Correct"
And I should not see "Other questions attempted here"
@javascript
Scenario: Redoing question 1 should save any changes to question 2 on the same page
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
When I press "Attempt quiz"
And I click on "False" "radio" in the "First question" "question"
And I click on "Check" "button" in the "First question" "question"
And I click on "True" "radio" in the "Second question" "question"
And I press "Try another question like this one"
And I click on "Check" "button" in the "Second question" "question"
Then the state of "Second question" question is shown as "Correct"
@javascript
Scenario: Redoing questions should work with random questions as well
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | canredoquestions |
| quiz | Quiz 2 | Quiz 2 description | C1 | quiz2 | immediatefeedback | 1 |
And I am on the "Quiz 2" "mod_quiz > Edit" page logged in as "admin"
And I open the "last" add to quiz menu
And I follow "a random question"
And I press "Add random question"
And user "student" has started an attempt at quiz "Quiz 2" randomised as follows:
| slot | actualquestion |
| 1 | TF1 |
And I am on the "Quiz 2" "mod_quiz > View" page logged in as "student"
When I press "Continue your attempt"
And I should see "First question"
And I click on "False" "radio"
And I click on "Check" "button"
And I press "Try another question like this one"
And I should see "Second question"
And "Check" "button" should exist
Scenario: Teachers reviewing can see author of action in review attempt
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext | answer 1 | grade |
| Test questions | shortanswer | SA1 | Who is author of Harry Potter? | J.K.Rowling | 100% |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 2 | Quiz 2 description | C1 | quiz2 |
And quiz "Quiz 2" contains the following questions:
| question | page |
| SA1 | 1 |
And user "student" has attempted "Quiz 2" with responses:
| slot | response |
| 1 | J.K.Rowling |
And I am on the "Quiz 2" "mod_quiz > Manual grading report" page logged in as "teacher"
And I follow "Also show questions that have been graded automatically"
When I click on "update grades" "link" in the "SA1" "table_row"
Then I set the field "Comment" to "I have adjusted your mark to 1.0"
And I set the field "Mark" to "1.0"
And I press "Save and show next"
And I follow "Results"
And I follow "Review attempt"
And I should see "Teacher One" in the "I have adjusted your mark to 1.0" "table_row"
@@ -0,0 +1,265 @@
@mod @mod_quiz
Feature: Attempt a quiz where some questions require that the previous question has been answered.
In order to complete a quiz where questions require previous ones to be complete
As a student
I need later questions to appear once earlier ones have been answered.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student | Student | One | student@example.com |
| teacher | Teacher | One | teacher@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student | C1 | student |
| teacher | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
@javascript
Scenario Outline: A question that requires the previous one is initially blocked
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | <quizbehaviour> |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 0 |
| TF2 | 1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
Then I should see "First question"
And I should see "This question cannot be attempted until the previous question has been completed."
And I should not see "Second question"
And I am on the "Quiz 1 > student > Attempt 1" "mod_quiz > Attempt review" page logged in as "teacher"
And I should see "First question"
And I should see "This question cannot be attempted until the previous question has been completed."
And I should not see "Second question"
And "Question 1" "link" should exist
And "Question 2" "link" should not exist
Examples:
| quizbehaviour |
| immediatefeedback |
| interactive |
@javascript
Scenario Outline: A question is shown as blocked when previewing a quiz
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | <quizbehaviour> |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 0 |
| TF2 | 1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "teacher"
And I press "Preview quiz"
Then I should see "First question"
And I should see "This question cannot be attempted until the previous question has been completed."
And I should not see "Second question"
Examples:
| quizbehaviour |
| immediatefeedback |
| interactive |
@javascript
Scenario Outline: A question requires the previous one becomes available when the first one is answered
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | <quizbehaviour> |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 0 |
| TF2 | 1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I click on "True" "radio" in the "First question" "question"
And I press "Check"
Then I should see "First question"
And I should not see "This question cannot be attempted until the previous question has been completed."
And I should see "Second question"
And "Question 1" "link" should exist
And "Question 2" "link" should exist
Examples:
| quizbehaviour |
| immediatefeedback |
| interactive |
@javascript
Scenario: After quiz submitted, all questions show on the review page
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 0 |
| TF2 | 1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
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 the state of "First question" question is shown as "Not answered"
And the state of "Second question" question is shown as "Not answered"
@javascript
Scenario: A questions cannot be blocked in a deferred feedback quiz (despite what is set in the DB).
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | deferredfeedback |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 0 |
| TF2 | 1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
Then I should see "First question"
And I should see "Second question"
And I should not see "This question cannot be attempted until the previous question has been completed."
@javascript
Scenario: Questions cannot be blocked in a shuffled section (despite what is set in the DB).
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | questionsperpage |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | 2 |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 2 | 1 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
Then I should see "First question"
And I should see "Second question"
And I should not see "This question cannot be attempted until the previous question has been completed."
@javascript
Scenario: Question dependency cannot apply to the first questions in section when the previous section is shuffled
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | questionsperpage |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | 2 |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 2 | 1 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 1 |
| Section 2 | 2 | 0 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I press "Next page"
Then I should see "Second question"
And I should not see "This question cannot be attempted until the previous question has been completed."
@javascript
Scenario: A questions cannot be blocked in sequential quiz (despite what is set in the DB).
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | navmethod |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | sequential |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
Then I should see "First question"
And I should see "Second question"
And I should not see "This question cannot be attempted until the previous question has been completed."
@javascript
Scenario: A questions not blocked if the previous one cannot finish, e.g. essay (despite what is set in the DB).
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | essay | Story | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| Story | 1 | 0 |
| TF2 | 1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
Then I should see "First question"
And I should see "Second question"
And I should not see "This question cannot be attempted until the previous question has been completed."
@javascript
Scenario: A questions not blocked if the previous one cannot finish, e.g. description (despite what is set in the DB).
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | description | Info | Read me |
| Test questions | truefalse | TF1 | First question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| Info | 1 | 0 |
| TF1 | 1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
Then I should see "Read me"
And I should see "First question"
And I should not see "This question cannot be attempted until the previous question has been completed."
@@ -0,0 +1,64 @@
@mod @mod_quiz
Feature: Allow settings to show Max marks and Marks, Max marks only, or hide the grade information completely.
As a teacher
In order to decide how grade review options are displayed on questions in a quiz review page
I need to be able to set the grade review options for a quiz to to show Max and Marks, Max only, or hide the grade information.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | One | student1@example.com |
| teacher | Teacher | One | teacher@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student1 | C1 | student |
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 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | maxmarksduring | marksduring | maxmarksimmediately | marksimmediately | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 test | C1 | quiz1 | 1 | 1 | 1 | 1 | immediatefeedback |
| quiz | Quiz 2 | Quiz 2 test | C1 | quiz2 | 0 | 0 | 1 | 1 | immediatefeedback |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | 2.00 |
And quiz "Quiz 2" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | 2.00 |
@javascript
Scenario: Show max marks and marks during and immediately after the attempt.
Given I am on the "Quiz 1" "quiz activity" page logged in as "student1"
And I click on "Attempt quiz" "button"
And I should see "Question 1" in the ".info" "css_element"
And I should see "Not complete" in the ".info" "css_element"
And I should see "Marked out of 2.00" in the ".info" "css_element"
And I set the field "True" 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 "Finished" in the "Status" "table_row"
And I should see "Question 1" in the ".info" "css_element"
And I should see "Correct" in the ".info" "css_element"
And I should see "Mark 2.00 out of 2.00" in the ".info" "css_element"
And I am on the "Quiz 2" "quiz activity" page
And I click on "Attempt quiz" "button"
And I should see "Question 1" in the ".info" "css_element"
And I should see "Not complete" in the ".info" "css_element"
And I should not see "Marked out of 2.00" in the ".info" "css_element"
And I set the field "True" 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"
And I should see "Finished" in the "Status" "table_row"
And I should see "Question 1" in the ".info" "css_element"
And I should see "Correct" in the ".info" "css_element"
And I should see "Mark 2.00 out of 2.00" in the ".info" "css_element"
@@ -0,0 +1,94 @@
@mod @mod_quiz
Feature: Attempt a quiz in a sequential mode
As a student I should not be able to see the previous questions
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student | Student | One | student@example.com |
| teacher | Teacher | One | teacher@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student | C1 | student |
| teacher | 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 |
| Test questions | truefalse | TF4 | Fourth question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | navmethod |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | sequential |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 2 | 1 |
| TF3 | 3 | 1 |
| TF4 | 4 | 1 |
@javascript
Scenario Outline: As a student I should not be able to navigate out of sequence if sequential navigation is on.
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I should see "First question"
When I am on the "Quiz 1 > student > Attempt 1 > <pagenumber>" "mod_quiz > Attempt view" page
And I should see "<canseequestion>"
Then I should not see "<cannotseequestion>"
Examples:
| pagenumber | canseequestion | cannotseequestion |
| 1 | First question | Second question |
| 2 | Second question | First question |
| 4 | First question | Fourth question |
@javascript
Scenario: As a student I should not be able to navigate out of sequence by opening new windows on the same quiz.
Given the following config values are set as admin:
| config | value | plugin |
| autosaveperiod | 60 | quiz |
And I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I should see "First question"
And I click on "True" "radio" in the "First question" "question"
And I click on "Next page" "button"
When I am on the "Quiz 1 > student > Attempt 1 > 3" "mod_quiz > Attempt view" page
And I click on "True" "radio" in the "Third question" "question"
And I should see "Third question"
And I click on "Next page" "button"
And I am on the "Quiz 1 > student > Attempt 1 > 1" "mod_quiz > Attempt view" page
Then I should see "Fourth question"
@javascript
Scenario: As a student I should not be able to save my data by opening a given page out of sequence.
Given the following config values are set as admin:
| config | value | plugin |
| autosaveperiod | 1 | quiz |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I am on the "Quiz 1 > student > Attempt 1 > 2" "mod_quiz > Attempt view" page
And I should see "Second question"
And I click on "True" "radio" in the "Second question" "question"
And I wait "2" seconds
And I am on the "Quiz 1 > student > Attempt 1 > 1" "mod_quiz > Attempt view" page
Then I should see "Second question"
@javascript
Scenario: As a student I can review question I have finished in any order
Given user "student" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | False |
| 3 | False |
| 4 | False |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I follow "Review"
And I am on the "Quiz 1 > student > Attempt 1 > 3" "mod_quiz > Attempt view" page
And I should see "Third question"
And I am on the "Quiz 1 > student > Attempt 1 > 2" "mod_quiz > Attempt view" page
Then I should see "Second question"
+78
View File
@@ -0,0 +1,78 @@
@mod @mod_quiz
Feature: Backup and restore of quizzes
In order to reuse my quizzes
As a teacher
I need to be able to back them up and restore them.
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following config values are set as admin:
| enableasyncbackup | 0 |
And I log in as "admin"
@javascript
Scenario: Duplicate a quiz with two questions
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | For testing backup | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
When I am on "Course 1" course homepage with editing mode on
And I duplicate "Quiz 1" activity editing the new copy with:
| Name | Quiz 2 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then I should see "TF1"
And I should see "TF2"
@javascript
Scenario: Backup and restore a course containing a quiz with user data.
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | For testing backup | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
And the following "users" exist:
| username |
| student |
And the following "course enrolments" exist:
| user | course | role |
| student | C1 | student |
And user "student" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | False |
When I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
| Schema | Course name | Restored course |
Then I should see "Restored course"
And I click on "Quiz 1" "link" in the "region-main" "region"
And I should see "Attempts: 1"
@javascript @_file_upload
Scenario: Restore a Moodle 2.8 quiz backup
When I am on the "Course 1" "restore" page
And I press "Manage course backups"
And I upload "mod/quiz/tests/fixtures/moodle_28_quiz.mbz" file to "Files" filemanager
And I press "Save changes"
And I restore "moodle_28_quiz.mbz" backup into "Course 1" course using this options:
And I am on the "Restored Moodle 2.8 quiz" "mod_quiz > Edit" page
Then I should see "TF1"
And I should see "TF2"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
@mod @mod_quiz @core_completion @javascript
Feature: Set a quiz to be marked complete when the student uses all attempts allowed
In order to ensure a student has learned the material before being marked complete
As a teacher
I need to set a quiz to complete when the student receives a passing grade, or completed_fail if they use all attempts without passing
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
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 | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | attempts | gradepass | completion | completionusegrade | completionpassgrade | completionattemptsexhausted |
| quiz | Test quiz name | C1 | quiz1 | 2 | 5.00 | 2 | 1 | 1 | 1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | False |
Scenario Outline: Student attempts the quiz - pass and fails
When I am on the "Course 1" course page logged in as student1
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "failed"
And the "Receive a pass grade or complete all available attempts" completion condition of "Test quiz name" is displayed as "todo"
And I follow "Test quiz name"
And I press "Re-attempt quiz"
And I set the field "<answer>" 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"
And I am on "Course 1" course homepage
Then the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "<passcompletionexpected>"
And the "Receive a pass grade or complete all available attempts" completion condition of "Test quiz name" is displayed as "done"
And I click on "Test quiz name" "link" in the "region-main" "region"
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "<passcompletionexpected>"
And the "Receive a pass grade or complete all available attempts" completion condition of "Test quiz name" is displayed as "done"
And I log out
And I am on the "Test quiz name" "quiz activity" page logged in as teacher1
And "Test quiz name" should have the "Receive a pass grade or complete all available attempts" completion condition
And I am on "Course 1" course homepage
And I navigate to "Reports" in current page administration
And I click on "Activity completion" "link"
And "<expectedactivitycompletion>" "icon" should exist in the "Student 1" "table_row"
Examples:
| answer | passcompletionexpected | expectedactivitycompletion |
| False | failed | Completed (did not achieve pass grade) |
| True | done | Completed (achieved pass grade) |
@@ -0,0 +1,58 @@
@mod @mod_quiz @core_completion @javascript
Feature: Set a quiz to be marked complete when the student completes a minimum amount of attempts
In order to ensure a student has completed the quiz before being marked complete
As a teacher
I need to set a quiz to complete when the student completes a certain amount of attempts
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
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 | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | completion | completionminattemptsenabled | completionminattempts |
| quiz | Test quiz name | C1 | quiz1 | 2 | 1 | 2 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | False |
Scenario: student1 uses up both attempts without passing
When I am on the "Course 1" course page logged in as teacher1
And "Completed: Test quiz name" "icon" should not exist in the "Test quiz name" "list_item"
And I log out
And I am on the "Course 1" course page logged in as student1
And the "Make attempts: 2" completion condition of "Test quiz name" is displayed as "todo"
And I click on "Test quiz name" "link" in the "region-main" "region"
And I press "Re-attempt quiz"
And I set the field "False" 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"
And I am on "Course 1" course homepage
Then the "Make attempts: 2" completion condition of "Test quiz name" is displayed as "done"
And I click on "Test quiz name" "link" in the "region-main" "region"
And the "Make attempts: 2" completion condition of "Test quiz name" is displayed as "done"
And I log out
And I am on the "Course 1" course page logged in as teacher1
And I click on "Test quiz name" "link" in the "region-main" "region"
And "Test quiz name" should have the "Make attempts: 2" completion condition
And I am on "Course 1" course homepage
And I navigate to "Reports" in current page administration
And I click on "Activity completion" "link"
And "Completed" "icon" should exist in the "Student 1" "table_row"
@@ -0,0 +1,89 @@
@mod @mod_quiz @core_completion
Feature: Set a quiz to be marked complete when the student passes
In order to ensure a student has learned the material before being marked complete
As a teacher
I need to set a quiz to complete when the student recieves a passing grade
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
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 | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | attempts | gradepass | completion | completionusegrade | completionpassgrade | completionview |
| quiz | Test quiz name | C1 | quiz1 | 4 | 5.00 | 2 | 1 | 1 | 1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
@javascript
Scenario: student1 passes on the first try
When I log in as "student1"
And I am on "Course 1" course homepage
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "todo"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "todo"
And the "View" completion condition of "Test quiz name" is displayed as "todo"
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | True |
And I follow "Test quiz name"
Then the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "done"
And the "View" completion condition of "Test quiz name" is displayed as "done"
And I am on "Course 1" course homepage
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "done"
And the "View" completion condition of "Test quiz name" is displayed as "done"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Reports" in current page administration
And I click on "Activity completion" "link"
And "Completed" "icon" should exist in the "Student 1" "table_row"
Scenario Outline: Verify that gradepass, together with completionpassgrade are validated correctly
Given the following "language customisations" exist:
| component | stringid | value |
| core_langconfig | decsep | <decsep> |
And the following "activity" exist:
| activity | name | course | idnumber | gradepass | completion | completionpassgrade |
| quiz | Oh, grades, passgrades and floats| C1 | ohgrades | <gradepass>| 2 | <completionpassgrade> |
When I am on the "ohgrades" "quiz activity editing" page logged in as "teacher1"
And I expand all fieldsets
And I set the field "Grade to pass" to "<gradepass>"
And I set the field "Add requirements" to "1"
And I set the field "Receive a grade" to "1"
And I set the field "<completionpassgrade>" to "1"
And I press "Save and display"
Then I should see "<seen>"
And I should not see "<notseen>"
Examples:
| gradepass | completionpassgrade | decsep | seen | notseen | outcome |
| | Any grade | . | method: Highest | Save and display | ok |
| | Passing grade | . | does not have a valid | method: Highest | completion-err |
| 0 | Any grade | . | method: Highest | Save and display | ok |
| 0 | Passing grade | . | does not have a valid | method: Highest | completion-err |
| aaa | Any grade | . | must enter a number | method: Highest | number-err |
| aaa | Passing grade | . | must enter a number | method: Highest | number-err |
| 200 | Any grade | . | can not be greater | method: Highest | grade-big-err |
| 200 | Passing grade | . | can not be greater | method: Highest | grade-big-err |
| 5.55 | Any grade | . | 5.55 out of 100 | Save and display | ok |
| 5.55 | Passing grade | . | 5.55 out of 100 | Save and display | ok |
| 5#55 | Any grade | . | must enter a number | method: Highest | number-err |
| 5#55 | Passing grade | . | must enter a number | method: Highest | number-err |
| 5#55 | Any grade | # | 5#55 out of 100 | Save and display | ok |
| 5#55 | Passing grade | # | 5#55 out of 100 | Save and display | ok |
@@ -0,0 +1,41 @@
@mod @mod_quiz @core_completion
Feature: Manually complete a quiz
In order to meet manual quiz completion requirements
As a student
I need to be able to view and modify my quiz manual completion status
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
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 | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | completion |
| quiz | Test quiz name | C1 | quiz1 | 1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
@javascript
Scenario: Use manual completion
Given I am on the "Test quiz name" "quiz activity" page logged in as teacher1
And the manual completion button for "Test quiz name" should be disabled
And I log out
# Student view.
When I am on the "Test quiz name" "quiz activity" page logged in as student1
Then the manual completion button of "Test quiz name" is displayed as "Mark as done"
And I toggle the manual completion state of "Test quiz name"
And the manual completion button of "Test quiz name" is displayed as "Done"
+254
View File
@@ -0,0 +1,254 @@
@mod @mod_quiz
Feature: Edit quiz page - adding things
In order to build the quiz I want my students to attempt
As a teacher
I need to be able to add questions to the quiz.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 for testing the Add menu | C1 | quiz1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
@javascript
Scenario: Add some new question to the quiz using '+ a new question' options of the 'Add' menu.
When I open the "last" add to quiz menu
And I follow "a new question"
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 01 new"
And I set the field "Question text" to "Please write 200 words about Essay 01"
And I press "id_submitbutton"
And I should see "Essay 01 new" on quiz page "1"
And I open the "Page 1" add to quiz menu
And I follow "a new question"
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 02 new"
And I set the field "Question text" to "Please write 200 words about Essay 02"
And I press "id_submitbutton"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I open the "Page 1" add to quiz menu
And I follow "a new question"
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 03 new"
And I set the field "Question text" to "Please write 300 words about Essay 03"
And I press "id_submitbutton"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I open the "Page 1" add to quiz menu
And I follow "a new question"
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 04 new"
And I set the field "Question text" to "Please write 300 words about Essay 04"
And I press "id_submitbutton"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I should see "Essay 04 new" on quiz page "1"
# Repaginate as two questions per page.
And I should not see "Page 2"
When I press "Repaginate"
Then I should see "Repaginate with"
And I set the field "menuquestionsperpage" to "2"
And I click on "Go" "button" in the "Repaginate" "dialogue"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "2"
And I should see "Essay 04 new" on quiz page "2"
# Add a question to page 2.
When I open the "Page 2" add to quiz menu
And I choose "a new question" in the open action menu
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay for page 2"
And I set the field "Question text" to "Please write 200 words about Essay for page 2"
And I press "id_submitbutton"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "2"
And I should see "Essay 04 new" on quiz page "2"
And I should see "Essay for page 2" on quiz page "2"
@javascript
Scenario: Add questions from question bank to the quiz. In order to be able to
add questions from question bank to the quiz, first we create some new questions
in various categories and add them to the question bank.
# Create a couple of sub categories.
When I am on the "Course 1" "core_question > course question categories" page
Then I should see "Add category"
And I follow "Add category"
Then I set the field "Parent category" to "Default for C1"
And I set the field "Name" to "Subcat 1"
And I set the field "Category info" to "This is sub category 1"
And I press "id_submitbutton"
And I should see "Subcat 1"
And I follow "Add category"
Then I set the field "Parent category" to "Default for C1"
And I set the field "Name" to "Subcat 2"
And I set the field "Category info" to "This is sub category 2"
And I press "id_submitbutton"
And I should see "Subcat 2"
And I select "Questions" from the "Question bank tertiary navigation" singleselect
And I should see "Question bank"
# Create the Essay 01 question.
When I press "Create a new question ..."
And I set the field "item_qtype_essay" to "1"
And I click on "Add" "button" in the "Choose a question type to add" "dialogue"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 01"
And I set the field "Question text" to "Please write 100 words about Essay 01"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "Essay 01"
# Create the Essay 02 question.
When I press "Create a new question ..."
And I set the field "item_qtype_essay" to "1"
And I click on "Add" "button" in the "Choose a question type to add" "dialogue"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 02"
And I set the field "Question text" to "Please write 200 words about Essay 02"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "Essay 02"
# Create the Essay 03 question.
And I wait until the page is ready
When I press "Create a new question ..."
And I set the field "item_qtype_essay" to "1"
And I click on "Add" "button" in the "Choose a question type to add" "dialogue"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 03"
And I set the field "Question text" to "Please write 300 words about Essay 03"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "Essay 03"
# Create the TF 01 question.
When I press "Create a new question ..."
And I set the field "item_qtype_truefalse" to "1"
And I click on "Add" "button" in the "Choose a question type to add" "dialogue"
Then I should see "Adding a True/False question"
And I set the field "Question name" to "TF 01"
And I set the field "Question text" to "The correct answer is true"
And I set the field "Correct answer" to "True"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "TF 01"
# Create the TF 02 question.
When I press "Create a new question ..."
And I set the field "item_qtype_truefalse" to "1"
And I click on "Add" "button" in the "Choose a question type to add" "dialogue"
Then I should see "Adding a True/False question"
And I set the field "Question name" to "TF 02"
And I set the field "Question text" to "The correct answer is false"
And I set the field "Correct answer" to "False"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "TF 02"
# Add questions from question bank using the Add menu.
# Add Essay 03 from question bank.
And I am on the "Quiz 1" "mod_quiz > Edit" page
And I open the "last" add to quiz menu
And I follow "from question bank"
Then the "Add selected questions to the quiz" "button" should be disabled
And I click on "Select" "checkbox" in the "Essay 03" "table_row"
Then the "Add selected questions to the quiz" "button" should be enabled
And I click on "Add to quiz" "link" in the "Essay 03" "table_row"
And I should see "Essay 03" on quiz page "1"
# Add Essay 01 from question bank.
And I open the "Page 1" add to quiz menu
And I follow "from question bank"
And I click on "Add to quiz" "link" in the "Essay 01" "table_row"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "1"
# Add Esay 02 from question bank.
And I open the "Page 1" add to quiz menu
And I follow "from question bank"
And I click on "Add to quiz" "link" in the "Essay 02" "table_row"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "1"
And I should see "Essay 02" on quiz page "1"
# Add a random question.
And I open the "Page 1" add to quiz menu
And I follow "a random question"
And I press "Add random question"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "1"
And I should see "Essay 02" on quiz page "1"
And I should see "Random" on quiz page "1"
# Repaginate as one question per page.
And I should not see "Page 2"
When I press "Repaginate"
Then I should see "Repaginate with"
And I set the field "menuquestionsperpage" to "1"
When I click on "Go" "button" in the "Repaginate" "dialogue"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "2"
And I should see "Essay 02" on quiz page "3"
And I should see "Random" on quiz page "4"
# Add a random question to page 4.
And I open the "Page 4" add to quiz menu
And I choose "a new question" in the open action menu
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay for page 4"
And I set the field "Question text" to "Please write 200 words about Essay for page 4"
And I press "id_submitbutton"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "2"
And I should see "Essay 02" on quiz page "3"
And I should see "Random" on quiz page "4"
And I should see "Essay for page 4" on quiz page "4"
@accessibility @javascript
Scenario: Check the accessibility of the quiz questions page
Given the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
| Test questions | truefalse | Other question | Answer the first question |
And quiz "Quiz 1" contains the following questions:
| question | page |
| First question | 1 |
When I reload the page
Then I should see "First question"
And the page should meet accessibility standards
@@ -0,0 +1,160 @@
@mod @mod_quiz @javascript
Feature: Adding questions to a quiz from the question bank
In order to re-use questions
As a teacher
I want to add questions from the question bank
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | weeks |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 for testing the Add menu | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | user | questiontext | idnumber |
| Test questions | essay | question 01 name | admin | Question 01 text | |
| Test questions | essay | question 02 name | teacher1 | Question 02 text | qidnum |
Scenario: The questions can be filtered by tag
Given I am on the "question 01 name" "core_question > edit" page logged in as teacher1
And I set the following fields to these values:
| Tags | foo |
And I press "id_submitbutton"
And I choose "Edit question" action for "question 02 name" in the question bank
And I set the following fields to these values:
| Tags | bar |
And I press "id_submitbutton"
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I open the "last" add to quiz menu
And I follow "from question bank"
Then I should see "foo" in the "question 01 name" "table_row"
And I should see "bar" in the "question 02 name" "table_row"
And I should see "qidnum" in the "question 02 name" "table_row"
When I apply question bank filter "Tag" with value "foo"
And I should see "question 01 name" in the "categoryquestions" "table"
And I should not see "question 02 name" in the "categoryquestions" "table"
Scenario: The question modal can be paginated
Given the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | My collection |
And 45 "questions" exist with the following data:
| questioncategory | My collection |
| qtype | essay |
| name | Feature question [count] |
| questiontext | Write about topic [count] |
| user | teacher1 |
# Sadly, the above step generates questions which sort like FQ1, FQ11, FQ12, ..., FQ19, FQ2, FQ20, ...
# so the expected paging behaviour is not immediately intuitive with 20 questions per page.
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as teacher1
And I open the "last" add to quiz menu
And I follow "from question bank"
And I should see "question 01 name" in the "categoryquestions" "table"
And I should see "question 02 name" in the "categoryquestions" "table"
And I should not see "Feature question" in the "categoryquestions" "table"
And I set the field "Category" to "My collection"
And I press "Apply filters"
And I wait until the page is ready
Then I should not see "question 01 name" in the "categoryquestions" "table"
And I should see "Feature question 1" in the "categoryquestions" "table"
And I should see "Feature question 27" in the "categoryquestions" "table"
And I should not see "Feature question 28" in the "categoryquestions" "table"
And I click on "2" "link" in the ".pagination" "css_element"
And I wait until the page is ready
And I should not see "Feature question 27" in the "categoryquestions" "table"
And I should see "Feature question 28" in the "categoryquestions" "table"
And I should see "Feature question 45" in the "categoryquestions" "table"
And I should not see "Feature question 5"
And I click on "3" "link" in the ".pagination" "css_element"
And I wait until the page is ready
And I should not see "Feature question 45" in the "categoryquestions" "table"
And I should see "Feature question 5"
And I should see "Feature question 9"
Scenario: After closing and reopening the modal, it still works
Given the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | My collection |
And the following "question" exists:
| questioncategory | My collection |
| qtype | essay |
| name | Feature question |
| questiontext | Write about topic |
| user | teacher1 |
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as teacher1
And I open the "last" add to quiz menu
And I follow "from question bank"
And I click on "Close" "button" in the "Add from the question bank at the end" "dialogue"
And I open the "last" add to quiz menu
And I follow "from question bank"
And I set the field "Category" to "My collection"
And I press "Apply filters"
Then I should see "Feature question"
Scenario: Questions are added in the right place with multiple sections
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | essay | question 03 name | question 03 text |
And quiz "Quiz 1" contains the following questions:
| question | page |
| question 01 name | 1 |
| question 02 name | 2 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 0 |
| Section 2 | 2 | 0 |
And I log in as "teacher1"
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I open the "Page 1" add to quiz menu
And I follow "from question bank"
And I set the field with xpath "//tr[contains(normalize-space(.), 'question 03 name')]//input[@type='checkbox']" to "1"
And I click on "Add selected questions to the quiz" "button"
Then I should see "question 03 name" on quiz page "1"
And I should see "question 01 name" before "question 03 name" on the edit quiz page
Scenario: Add several selected questions from the question bank
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "from question bank"
And I set the field with xpath "//input[@type='checkbox' and @id='qbheadercheckbox']" to "1"
And I press "Add selected questions to the quiz"
Then I should see "question 01 name" on quiz page "1"
And I should see "question 02 name" on quiz page "2"
@javascript
Scenario: Validate the sorting while adding questions from question bank
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | multichoice | question 03 name | question 03 name text |
And I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "from question bank"
And I click on "Sort by Question ascending" "link"
Then "question 01 name" "text" should appear before "question 02 name" "text"
And I click on "Sort by Question descending" "link"
And "question 03 name" "text" should appear before "question 01 name" "text"
And I follow "Sort by Question type ascending"
Then "question 01 name" "text" should appear before "question 03 name" "text"
And I follow "Sort by Question type descending"
Then "question 03 name" "text" should appear before "question 01 name" "text"
Scenario: Shuffle option could be set before adding any question to the quiz
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | essay | question 03 name | question 03 text |
And I log in as "teacher1"
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I set the field "Shuffle" to "1"
And I open the "last" add to quiz menu
And I follow "from question bank"
Then I should see "question 01 name"
@@ -0,0 +1,120 @@
@mod @mod_quiz @javascript
Feature: Adding random questions to a quiz based on category and tags
In order to have better assessment
As a teacher
I want to display questions that are randomly picked from the question bank
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | t1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 for testing the Add random question form | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Questions Category 1 |
| Course | C1 | Questions Category 2 |
And the following "question categories" exist:
| contextlevel | reference | name | questioncategory |
| Course | C1 | Subcategory | Questions Category 1 |
And the following "questions" exist:
| questioncategory | qtype | name | user | questiontext |
| Questions Category 1 | essay | question 1 name | admin | Question 1 text |
| Questions Category 1 | essay | question 2 name | teacher1 | Question 2 text |
| Subcategory | essay | question 3 name | teacher1 | Question 3 text |
| Subcategory | essay | question 4 name | teacher1 | Question 4 text |
| Questions Category 1 | essay | "listen" & "answer" | teacher1 | Question 5 text |
And the following "core_question > Tags" exist:
| question | tag |
| question 1 name | foo |
| question 2 name | bar |
| question 3 name | foo |
| question 4 name | bar |
| "listen" & "answer" | foo |
Scenario: Available tags are shown in the autocomplete tag field
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I add question bank filter "Tag"
And I click on "Tag" "field"
And I press the down key
Then "foo" "autocomplete_suggestions" should exist
And "bar" "autocomplete_suggestions" should exist
Scenario: Questions can be filtered by tags
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I apply question bank filter "Tag" with value "foo"
And I wait until the page is ready
And I should see "question 1 name"
And I should see "\"listen\" & \"answer\""
And I should not see "question 2 name"
And I should not see "question 3 name"
And I should not see "question 4 name"
# Ensure tagged questions inside subcategories are also matched.
And I set the field "Also show questions from subcategories" to "1"
And I click on "Apply filters" "button"
And I wait until the page is ready
And I should see "question 1 name"
And I should see "question 3 name"
And I should see "\"listen\" & \"answer\""
And I should not see "question 2 name"
And I should not see "question 4 name"
Scenario: A random question can be added to the quiz
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I apply question bank filter "Tag" with value "foo"
And I select "1" from the "randomcount" singleselect
And I press "Add random question"
And I should see "Random (Questions Category 1) based on filter condition with tags: foo" on quiz page "1"
When I click on "Configure question" "link" in the "Random (Questions Category 1) based on filter condition with tags: foo" "list_item"
Then I should see "Questions Category 1"
And I should see "foo"
And I should see "question 1 name"
And I should see "\"listen\" & \"answer\""
Scenario: After closing and reopening the modal, it still works
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as teacher1
And I open the "last" add to quiz menu
And I follow "a random question"
And I click on "Close" "button" in the "Add a random question at the end" "dialogue"
And I open the "last" add to quiz menu
And I follow "a random question"
And I should not see "question 3 name"
And I set the field "Category" to "Subcategory"
And I press "Apply filters"
Then I should see "question 3 name"
Scenario: Teacher without moodle/question:useall should not see the add a random question menu item
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/question:useall | Prevent | editingteacher | Course | C1 |
And I log in as "teacher1"
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I open the "last" add to quiz menu
Then I should not see "a random question"
Scenario: A random question can be added to the quiz by creating a new category
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I follow "New category"
And "Help with Parent category" "icon" should exist in the "Random question using a new category" "fieldset"
And I set the following fields to these values:
| Name | New Random category |
| Parent category | Default for Quiz 1 |
And I press "Create category and add random question"
And I should see "Random (New Random category) based on filter condition" on quiz page "1"
And I click on "Configure question" "link" in the "Random (New Random category) based on filter condition" "list_item"
Then I should see "New Random category"
@@ -0,0 +1,54 @@
@mod @mod_quiz @javascript
Feature: Editing random questions already in a quiz based on category and tags
In order to have better assessment
As a teacher
I want to be able to update how questions are randomly picked from the question bank
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | t1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 for testing the Add random question form | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Questions Category 1|
| Course | C1 | Questions Category 2|
And the following "questions" exist:
| questioncategory | qtype | name | user | questiontext |
| Questions Category 1 | essay | question 1 name | admin | Question 1 text |
| Questions Category 1 | essay | question 2 name | teacher1 | Question 2 text |
And the following "core_question > Tags" exist:
| question | tag |
| question 1 name | easy |
| question 1 name | essay |
| question 2 name | hard |
| question 2 name | essay |
Scenario: Editing tags on one slot does not delete the rest
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
And I open the "last" add to quiz menu
And I follow "a random question"
# To actually reproduce MDL-68733 it would be better to set tags easy,essay here, and then below just delete one tag.
# However, the state of Behat for autocomplete fields does not let us actually do that.
And I apply question bank filter "Tag" with value "easy"
And I press "Add random question"
And I open the "Page 1" add to quiz menu
And I follow "a random question"
And I apply question bank filter "Tag" with value "hard"
And I press "Add random question"
And I follow "Add page break"
When I click on "Configure question" "link" in the "Random (Questions Category 1) based on filter condition with tags: easy" "list_item"
And I apply question bank filter "Tag" with value "essay"
And I press "Update filter conditions"
Then I should see "Random (Questions Category 1) based on filter condition with tags: essay" on quiz page "1"
And I should see "Random (Questions Category 1) based on filter condition with tags: hard" on quiz page "2"
And I click on "Configure question" "link" in the "Random (Questions Category 1) based on filter condition with tags: hard" "list_item"
And "hard" "autocomplete_selection" should be visible
@@ -0,0 +1,101 @@
@mod @mod_quiz
Feature: Edit quiz page - drag-and-drop
In order to change the layout of a quiz I built
As a teacher
I need to be able to drag and drop questions to reorder them.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| 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 | Question A | This is question 01 |
| Test questions | truefalse | Question B | This is question 02 |
| Test questions | truefalse | Question C | This is question 03 |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 1 |
| Question C | 2 |
And I log in as "teacher1"
And I am on the "Quiz 1" "mod_quiz > Edit" page
@javascript
Scenario: Re-order questions by clicking on the move icon.
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "2"
When I move "Question A" to "After Question 2" in the quiz by clicking the move icon
Then I should see "Question B" on quiz page "1"
And I should see "Question A" on quiz page "1"
And I should see "Question B" before "Question A" on the edit quiz page
And I should see "Question C" on quiz page "2"
When I move "Question A" to "After Page 2" in the quiz by clicking the move icon
Then I should see "Question B" on quiz page "1"
And I should see "Question A" on quiz page "2"
And I should see "Question C" on quiz page "2"
And I should see "Question A" before "Question C" on the edit quiz page
When I move "Question B" to "After Question 2" in the quiz by clicking the move icon
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "1"
And I should see "Question A" before "Question B" on the edit quiz page
And I should see "Question B" before "Question C" on the edit quiz page
When I move "Question B" to "After Page 1" in the quiz by clicking the move icon
Then I should see "Question B" on quiz page "1"
And I should see "Question A" on quiz page "1"
And I should see "Question C" on quiz page "1"
And I should see "Question B" before "Question A" on the edit quiz page
And I should see "Question A" before "Question C" on the edit quiz page
When I click on the "Add" page break icon after question "Question A"
When I open the "Page 2" add to quiz menu
And I choose "a new question" in the open action menu
And I set the field "item_qtype_description" to "1"
And I press "submitbutton"
Then I should see "Adding a description"
And I set the following fields to these values:
| Question name | Question D |
| Question text | Useful info |
And I press "id_submitbutton"
Then I should see "Question B" on quiz page "1"
And I should see "Question A" on quiz page "1"
And I should see "Question C" on quiz page "2"
And I should see "Question D" on quiz page "2"
And I should see "Question B" before "Question A" on the edit quiz page
And I should see "Question C" before "Question D" on the edit quiz page
And "Question B" should have number "1" on the edit quiz page
And "Question A" should have number "2" on the edit quiz page
And "Question C" should have number "3" on the edit quiz page
And "Question D" should have number "i" on the edit quiz page
When I move "Question D" to "After Question 2" in the quiz by clicking the move icon
Then I should see "Question B" on quiz page "1"
And I should see "Question D" on quiz page "1"
And I should see "Question A" on quiz page "1"
And I should see "Question C" on quiz page "2"
And I should see "Question B" before "Question A" on the edit quiz page
And I should see "Question A" before "Question D" on the edit quiz page
And "Question B" should have number "1" on the edit quiz page
And "Question D" should have number "i" on the edit quiz page
And "Question A" should have number "2" on the edit quiz page
And "Question C" should have number "3" on the edit quiz page
@@ -0,0 +1,177 @@
@mod @mod_quiz
Feature: Setup multiple grades for a quiz
In order to assess multiple things in one quiz
As a teacher
I need to be able to create multiple quiz grade items.
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 | questiontext |
| Test questions | description | Info | Some information |
| Test questions | truefalse | Question A | This is question 01 |
| Test questions | truefalse | Question B | This is question 02 |
| Test questions | truefalse | Question C | This is question 03 |
And the following "activities" exist:
| activity | name | course |
| quiz | Quiz 1 | C1 |
@javascript
Scenario: Navigation to, and display of, grading setup
Given the following "mod_quiz > grade items" exist:
| quiz | name |
| Quiz 1 | Intuition |
| Quiz 1 | Intelligence |
| Quiz 1 | Unused grade item |
And quiz "Quiz 1" contains the following questions:
| question | page | grade item |
| Info | 1 | |
| Question A | 1 | Intuition |
| Question B | 1 | Intelligence |
| Question C | 2 | Intuition |
When I am on the "Quiz 1" "mod_quiz > multiple grades setup" page logged in as teacher
Then I should see "Grade items"
And "Delete" "icon" should not exist in the "Intuition" "table_row"
And "Intuition" row "Total of marks" column of "mod_quiz-grade-item-list" table should contain "2.00"
And "Delete" "icon" should not exist in the "Intelligence" "table_row"
And "Intelligence" row "Total of marks" column of "mod_quiz-grade-item-list" table should contain "1.00"
And "Delete" "icon" should exist in the "Unused grade item" "table_row"
And "Unused grade item" row "Total of marks" column of "mod_quiz-grade-item-list" table should contain "-"
And the field "Question A" matches value "Intuition"
And "1" row "Marks" column of "mod_quiz-slot-list" table should contain "1.00"
And the field "Question B" matches value "Intelligence"
And "2" row "Marks" column of "mod_quiz-slot-list" table should contain "1.00"
And the field "Question C" matches value "Intuition"
And "3" row "Marks" column of "mod_quiz-slot-list" table should contain "1.00"
And I should not see "Info"
@javascript
Scenario: A grade item can be created and renamed
Given quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
When I am on the "Quiz 1" "mod_quiz > multiple grades setup" page logged in as teacher
And I should see "Create grade items within your quiz. Allocate questions or quiz sections to these grade items to break down grade results into different areas."
And I press "Add grade item"
Then "New grade item 1" "table_row" should exist
And I press "Add grade item"
Then "New grade item 2" "table_row" should exist
And I click on "Edit" "link" in the "New grade item 1" "table_row"
And I set the field "New name for grade item" to "Intelligence"
And I press enter
And I should not see "New grade item 1"
And "Intelligence" "table_row" should exist
@javascript
Scenario: Editing the name of a grade item can be cancelled
Given the following "mod_quiz > grade items" exist:
| quiz | name |
| Quiz 1 | Intuition |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
When I am on the "Quiz 1" "mod_quiz > multiple grades setup" page logged in as teacher
And I click on "Edit" "link" in the "Intuition" "table_row"
And I set the field "New name for grade item" to "Intelligence"
And I press the escape key
And I should not see "Intelligence"
And "Intuition" "table_row" should exist
@javascript
Scenario: Unused grade items can be deleted
Given the following "mod_quiz > grade items" exist:
| quiz | name |
| Quiz 1 | Unused grade item |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
When I am on the "Quiz 1" "mod_quiz > multiple grades setup" page logged in as teacher
And I follow "Delete grade item Unused grade item"
Then I should not see "Unused grade item"
And I should see "Create grade items within your quiz. Allocate questions or quiz sections to these grade items to break down grade results into different areas."
@javascript
Scenario: Grade item for a slot can be changed
Given the following "mod_quiz > grade items" exist:
| quiz | name |
| Quiz 1 | Intuition |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
When I am on the "Quiz 1" "mod_quiz > multiple grades setup" page logged in as teacher
And "Delete" "icon" should exist in the "Intuition" "table_row"
And I set the field "Question A" to "Intuition"
Then "Delete" "icon" should not exist in the "Intuition" "table_row"
And the field "Question A" matches value "Intuition"
And I set the field "Question A" to "[none]"
And "Delete" "icon" should exist in the "Intuition" "table_row"
And the field "Question A" matches value "[none]"
@javascript
Scenario: All setup can be reset
Given the following "mod_quiz > grade items" exist:
| quiz | name |
| Quiz 1 | Intuition |
| Quiz 1 | Intelligence |
| Quiz 1 | Unused grade item |
And quiz "Quiz 1" contains the following questions:
| question | page | grade item |
| Question A | 1 | Intuition |
| Question B | 1 | Intelligence |
| Question C | 2 | Intuition |
When I am on the "Quiz 1" "mod_quiz > multiple grades setup" page logged in as teacher
And I press "Reset setup"
And I click on "Reset" "button" in the "Reset grade items setup?" "dialogue"
Then I should see "Create grade items within your quiz. Allocate questions or quiz sections to these grade items to break down grade results into different areas."
And the field "Question A" matches value "[none]"
And the field "Question B" matches value "[none]"
And the field "Question C" matches value "[none]"
And I should not see "Reset grade items setup"
@javascript
Scenario: Reset all can be cancelled
Given the following "mod_quiz > grade items" exist:
| quiz | name |
| Quiz 1 | Intuition |
When I am on the "Quiz 1" "mod_quiz > multiple grades setup" page logged in as teacher
And I press "Reset setup"
And I click on "Cancel" "button" in the "Reset grade items 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 "Set up 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"
@@ -0,0 +1,137 @@
@mod @mod_quiz @javascript
Feature: Editing question numbering of the existing questions already in a quiz
In order to have better assessment
As a teacher
I want to be able to customided question numbering on the quiz editing page
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | t1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 for testing | C1 | quiz1 |
| quiz | Quiz 2 | Quiz 2 for testing | C1 | quiz2 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Questions Category 1|
| Course | C1 | Questions Category 2|
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Questions Category 1 | description | Description | This is decription ... |
| Questions Category 1 | truefalse | Question A | This is question 01 |
| Questions Category 1 | truefalse | Question B | This is question 02 |
| Questions Category 1 | truefalse | Question C | This is question 03 |
| Questions Category 1 | truefalse | Question D | This is question 04 |
And quiz "Quiz 1" contains the following questions:
| question | page | displaynumber |
| Description | 1 | |
| Question A | 1 | 1.a |
| Question B | 1 | 1.b |
| Question C | 2 | |
| Question D | 2 | |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 0 |
| Section 2 | 4 | 0 |
Scenario: Showing customised and default question numbers on quiz editing page.
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
And I should see "Section 1"
And I should see "i" on quiz page "1"
And I should see "1.a" on quiz page "1"
And I should see "1.b" on quiz page "1"
And I should see "Section 2"
And I should see "3" on quiz page "2"
And I should see "4" on quiz page "2"
Scenario: Showing customised and default question numbers on quiz view page and question navigation.
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "teacher1"
When I press "Preview quiz"
Then I should see "Section 1" in the "Quiz navigation" "block"
And I should see question "1.a" in section "Section 1" in the quiz navigation
And I should see question "1.b" in section "Section 1" in the quiz navigation
And I should see "Section 2" in the "Quiz navigation" "block"
And I should see question "3" in section "Section 2" in the quiz navigation
And I should see question "4" in section "Section 2" in the quiz navigation
And I should see "Question 1.a"
And I should see "Question 1.b"
And I press "Next page"
And I should see "Question 3"
And I should see "Question 4"
Scenario: Customised numbers are not used in shuffled sections, even if they exist in the database
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
And I set the field "Shuffle" to "1"
When I am on the "Quiz 1" "mod_quiz > View" page
And I press "Preview quiz"
Then I should see "Section 1" in the "Quiz navigation" "block"
And I should see question "1" in section "Section 1" in the quiz navigation
And I should see question "2" in section "Section 1" in the quiz navigation
And I should see "Section 2" in the "Quiz navigation" "block"
And I should see question "3" in section "Section 2" in the quiz navigation
And I should see question "4" in section "Section 2" in the quiz navigation
And I should see "Question 1"
And I should see "Question 2"
And I press "Next page"
And I should see "Question 3"
And I should see "Question 4"
Scenario: Showing long customised question numbers on quiz editing page and parcially hidden on question info and navigation.
Given quiz "Quiz 2" contains the following questions:
| question | page | displaynumber |
| Question A | 1 | ABCDEFGHIJKLMNOP |
| Question B | 2 | abcdefghijklmnop |
And quiz "Quiz 2" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 0 |
| Section 2 | 2 | 0 |
When I am on the "Quiz 2" "mod_quiz > Edit" page logged in as "teacher1"
And I should see "ABCDEFGHIJKLMNOP" on quiz page "1"
And I should see "abcdefghijklmnop" on quiz page "2"
And I am on the "Quiz 2" "mod_quiz > View" page logged in as "teacher1"
And I press "Preview quiz"
# Only "Question ABCDEFGH" is visible in the question info box.
And I should see "Question ABCDEFGHIJKLMNOP"
# Only 'ABC' is visible on the navigation button/link.
And I should see question "ABCDEFGHIJKLMNOP" in section "Section 1" in the quiz navigation
And I press "Next page"
# Only "Question abcdefghij" is visible in the question info box.
And I should see "Question abcdefghijklmnop"
# Only 'abc' is visible on the navigation button/link.
And I should see question "abcdefghijklmnop" in section "Section 2" in the quiz navigation
Scenario: Shuffling questions within a section with customised question numbers.
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
Then I should see "Section 1"
And I should see "i" on quiz page "1"
And I should see "1.a" on quiz page "1"
And I should see "1.b" on quiz page "1"
And I should see "Section 2"
And I should see "3" on quiz page "2"
And I should see "4" on quiz page "2"
And I set the field "Shuffle" to "1"
And I should see "Section 1"
And I should see "i" on quiz page "1"
And I should see "1" on quiz page "1"
And I should see "2" on quiz page "1"
And I should see "Section 2"
And I should see "3" on quiz page "2"
And I should see "4" on quiz page "2"
And I reload the page
And I set the field "Shuffle" to "0"
And I should see "Section 1"
And I should see "i" on quiz page "1"
And I should see "1.a" on quiz page "1"
And I should see "1.b" on quiz page "1"
@@ -0,0 +1,295 @@
@mod @mod_quiz
Feature: Edit quiz page - remove multiple questions
In order to change the layout of a quiz I built efficiently
As a teacher
I need to be able to delete many questions questions.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And I log in as "teacher1"
@javascript
Scenario: Delete selected question using select multiple items feature.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | description | Info | Some instructions |
| Test questions | truefalse | Question A | This is question 01 |
| Test questions | truefalse | Question B | This is question 02 |
| Test questions | truefalse | Question C | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Info | 1 |
| Question A | 1 |
| Question B | 1 |
| Question C | 2 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
# Confirm the starting point.
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "2"
And I should see "Total of marks: 3.00"
And I should see "Questions: 4"
And I should see "This quiz is open"
# Delete last question in last page. Page contains multiple questions. No reordering.
When I click on "Select multiple items" "button"
Then I click on "selectquestion-3" "checkbox"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should see "Info" on quiz page "1"
And I should see "Question A" on quiz page "1"
And I should not see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "2"
And I should see "Total of marks: 2.00"
And I should see "Questions: 3"
@javascript
Scenario: Delete first selected question using select multiple items feature.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | This is question 01 |
| Test questions | truefalse | Question B | This is question 02 |
| Test questions | truefalse | Question C | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 2 |
| Question C | 2 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
# Confirm the starting point.
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "2"
And I should see "Question C" on quiz page "2"
And I should see "Total of marks: 3.00"
And I should see "Questions: 3"
And I should see "This quiz is open"
# Delete first question in first page. Page contains multiple questions. No reordering.
When I click on "Select multiple items" "button"
Then I click on "selectquestion-1" "checkbox"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "1"
And I should see "Total of marks: 2.00"
And I should see "Questions: 2"
@javascript
Scenario: Can delete the last question in a quiz.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | This is question 01 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I click on "Select multiple items" "button"
And I click on "selectquestion-1" "checkbox"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should see "Questions: 0"
@javascript
Scenario: Delete all questions by checking select all.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | This is question 01 |
| Test questions | truefalse | Question B | This is question 02 |
| Test questions | truefalse | Question C | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 1 |
| Question C | 2 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
# Confirm the starting point.
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "2"
And I should see "Total of marks: 3.00"
And I should see "Questions: 3"
And I should see "This quiz is open"
# Delete all questions in page. Page contains multiple questions
When I click on "Select multiple items" "button"
Then I press "Select all"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Question A" on quiz page "1"
And I should not see "Question B" on quiz page "1"
And I should not see "Question C" on quiz page "2"
And I should see "Total of marks: 0.00"
And I should see "Questions: 0"
@javascript
Scenario: Deselect all questions by checking deselect all.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | This is question 01 |
| Test questions | truefalse | Question B | This is question 02 |
| Test questions | truefalse | Question C | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 1 |
| Question C | 2 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
# Confirm the starting point.
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "2"
# Delete last question in last page. Page contains multiple questions
When I click on "Select multiple items" "button"
And I press "Select all"
Then the field "selectquestion-3" matches value "1"
When I press "Deselect all"
Then the field "selectquestion-3" matches value "0"
@javascript
Scenario: Delete multiple questions from sections
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | First question |
| Test questions | truefalse | Question B | Second question |
| Test questions | truefalse | Question C | Third question |
| Test questions | truefalse | Question D | Fourth question |
| Test questions | truefalse | Question E | Fifth question |
| Test questions | truefalse | Question F | Sixth question |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 2 |
| Question C | 3 |
| Question D | 4 |
| Question E | 5 |
| Question F | 6 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 0 |
| Section 2 | 2 | 0 |
| Section 3 | 4 | 0 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I click on "Select multiple items" "button"
And I click on "selectquestion-3" "checkbox"
And I click on "selectquestion-5" "checkbox"
And I click on "selectquestion-6" "checkbox"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "2"
And I should see "Question D" on quiz page "3"
And I should not see "Question C"
And I should not see "Question E"
And I should not see "Question F"
@javascript
Scenario: Attempting to delete all questions of a sections
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | First question |
| Test questions | truefalse | Question B | Second question |
| Test questions | truefalse | Question C | Third question |
| Test questions | truefalse | Question D | Fourth question |
| Test questions | truefalse | Question E | Fifth question |
| Test questions | truefalse | Question F | Sixth question |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 2 |
| Question C | 3 |
| Question D | 4 |
| Question E | 5 |
| Question F | 6 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 0 |
| Section 2 | 2 | 0 |
| Section 3 | 4 | 0 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I click on "Select multiple items" "button"
And I click on "selectquestion-2" "checkbox"
And I click on "selectquestion-3" "checkbox"
And I click on "Delete selected" "button"
Then I should see "Cannot remove questions"
@javascript
Scenario: Delete multiple random questions from sections.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | First question |
| Test questions | truefalse | Question B | Second question |
| Test questions | truefalse | Question C | Third question |
| Test questions | truefalse | Question D | Fourth question |
| Test questions | truefalse | Question E | Fifth question |
| Test questions | truefalse | Question F | Sixth question |
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I open the "last" add to quiz menu
And I follow "a random question"
And I set the field "Number of random questions" to "3"
And I press "Add random question"
And I click on "Select multiple items" "button"
And I click on "selectquestion-1" "checkbox"
And I click on "selectquestion-2" "checkbox"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should see "Random (Test questions) based on filter condition" on quiz page "1"
And I should not see "Random (Test questions) based on filter condition" on quiz page "2"
And I should not see "Random (Test questions) based on filter condition" on quiz page "3"
And I should see "Total of marks: 1.00"
And I should see "Questions: 1"
@javascript
Scenario: Delete all random questions by checking select all.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | First question |
| Test questions | truefalse | Question B | Second question |
| Test questions | truefalse | Question C | Third question |
| Test questions | truefalse | Question D | Fourth question |
| Test questions | truefalse | Question E | Fifth question |
| Test questions | truefalse | Question F | Sixth question |
And I am on the "Quiz 1" "mod_quiz > Edit" page
# Delete all questions in page. Page contains multiple questions.
When I open the "last" add to quiz menu
And I follow "a random question"
And I set the field "Number of random questions" to "3"
And I press "Add random question"
And I click on "Select multiple items" "button"
And I press "Select all"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should not see "Random question based on filter condition" on quiz page "1"
And I should not see "Random question based on filter condition" on quiz page "2"
And I should not see "Random question based on filter condition" on quiz page "3"
And I should see "Total of marks: 0.00"
And I should see "Questions: 0"
@@ -0,0 +1,97 @@
@mod @mod_quiz
Feature: Edit quiz page - remove questions
In order to change the layout of a quiz I built
As a teacher
I need to be able to delete questions.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And I log in as "teacher1"
@javascript
Scenario: Delete questions by clicking on the delete icon.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | This is question 01 |
| Test questions | truefalse | Question B | This is question 02 |
| Test questions | truefalse | Question C | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 1 |
| Question C | 2 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
# Confirm the starting point.
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "2"
And I should see "Total of marks: 3.00"
And I should see "Questions: 3"
And I should see "This quiz is open"
# Delete last question in last page. Page contains multiple questions
When I delete "Question C" in the quiz by clicking the delete icon
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should not see "Question C" on quiz page "2"
And I should see "Total of marks: 2.00"
And I should see "Questions: 2"
# Delete last question in last page. The page contains multiple questions and there are multiple pages.
When I click on the "Add" page break icon after question "Question A"
Then I should see "Question B" on quiz page "2"
And the "Remove" page break icon after question "Question A" should exist
And I delete "Question A" in the quiz by clicking the delete icon
Then I should see "Question B" on quiz page "1"
And I should not see "Page 2"
And I should not see "Question A" on quiz page "2"
And the "Remove" page break icon after question "Question B" should not exist
And I should see "Total of marks: 1.00"
@javascript
Scenario: Cannot delete the last question in a section.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | This is question 01 |
| Test questions | truefalse | Question B | This is question 02 |
| Test questions | truefalse | Question C | This is question 03 |
And 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 |
| Heading 1 | 1 | 1 |
| Heading 2 | 2 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "Delete" "link" in the "Question A" "list_item" should not be visible
Then "Delete" "link" in the "Question B" "list_item" should be visible
Then "Delete" "link" in the "Question C" "list_item" should be visible
@javascript
Scenario: Can delete the last question in a quiz.
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | This is question 01 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I delete "Question A" in the quiz by clicking the delete icon
Then I should see "Questions: 0"
@@ -0,0 +1,146 @@
@mod @mod_quiz
Feature: Edit quiz page - pagination
In order to build a quiz laid out in pages the way I want
As a teacher
I need to be able to add and remove pages, and repaginate.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
When I log in as "teacher1"
And I am on the "Quiz 1" "mod_quiz > Edit" page
@javascript
Scenario: Repaginate questions with N question(s) per page as well as clicking
on "add page break" or "Remove page break" icons to repaginate in any desired format.
# Add the first Essay question.
And I open the action menu in ".page-add-actions" "css_element"
And I follow "a new question"
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 01 new"
And I set the field "Question text" to "Please write 100 words about Essay 01"
And I press "id_submitbutton"
Then I should see "Essay 01 new" on quiz page "1"
# Add the second Essay question.
And I open the action menu in ".page-add-actions" "css_element"
And I follow "a new question"
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 02 new"
And I set the field "Question text" to "Please write 200 words about Essay 02"
And I press "id_submitbutton"
Then I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
# Start repaginating.
And I should not see "Page 2"
When I click on the "Add" page break icon after question "Essay 01 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
When I click on the "Remove" page break icon after question "Essay 01 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should not see "Page 2"
# Add the third Essay question.
And I open the action menu in ".page-add-actions" "css_element"
And I follow "a new question"
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 03 new"
And I set the field "Question text" to "Please write 200 words about Essay 03"
And I press "id_submitbutton"
Then I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I should not see "Page 2"
And I should not see "Page 3"
When I click on the "Add" page break icon after question "Essay 02 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "2"
And I should not see "Page 3"
When I click on the "Add" page break icon after question "Essay 01 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "3"
When I click on the "Remove" page break icon after question "Essay 02 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "2"
And I should not see "Page 3"
When I click on the "Remove" page break icon after question "Essay 01 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I should not see "Page 2"
And I should not see "Page 3"
# Repaginate one question per page.
When I press "Repaginate"
And I set the field "menuquestionsperpage" to "1"
And I click on "Go" "button" in the "Repaginate" "dialogue"
Then I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "3"
# Add the forth Essay question in a new page (Page 4).
When I open the "Page 3" add to quiz menu
And I choose "a new question" in the open action menu
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
When I set the field "Question name" to "Essay 04 new"
And I set the field "Question text" to "Please write 300 words about Essay 04"
And I press "id_submitbutton"
Then I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "3"
And I should see "Essay 04 new" on quiz page "3"
When I click on the "Add" page break icon after question "Essay 03 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "3"
And I should see "Essay 04 new" on quiz page "4"
# Repaginate with 2 questions per page.
When I press "Repaginate"
And I set the field "menuquestionsperpage" to "2"
And I click on "Go" "button" in the "Repaginate" "dialogue"
Then I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "2"
And I should see "Essay 04 new" on quiz page "2"
# Repaginate with unlimited questions per page (All questions on Page 1).
When I press "Repaginate"
And I set the field "menuquestionsperpage" to "Unlimited"
And I click on "Go" "button" in the "Repaginate" "dialogue"
Then I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I should see "Essay 04 new" on quiz page "1"
@@ -0,0 +1,234 @@
@mod @mod_quiz
Feature: Edit quizzes where some questions require the previous one to have been completed
In order to create quizzes where later questions can only be seen after earlier ones are answered
As a teacher
I need to be able to configure this on the Edit quiz page
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And I log in as "teacher1"
@javascript
Scenario: The first question cannot depend on the previous (whatever is in the DB)
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "be attempted" "link" should not be visible
# The text "be attempted" is used as a relatively unique string in both the add and remove links.
@javascript
Scenario: If the second question depends on the first, that is shown
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 0 |
| TF2 | 1 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "This question cannot be attempted until the previous question has been completed." "link" should be visible
@javascript
Scenario: A question can depend on a random question
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | random | Random (Test questions) | 0 |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| Random (Test questions) | 1 | 0 |
| TF1 | 1 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "This question cannot be attempted until the previous question has been completed." "link" should be visible
@javascript
Scenario: The second question can be set to depend on the first
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
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 quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 0 |
| TF2 | 1 | 0 |
| TF3 | 1 | 0 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I follow "No restriction on when question 2 can be attempted Click to change"
Then "Question 2 cannot be attempted until the previous question 1 has been completed Click to change" "link" should be visible
And "No restriction on when question 3 can be attempted Click to change" "link" should be visible
@javascript
Scenario: A question that did depend on the previous can be un-linked
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
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 quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 0 |
| TF2 | 1 | 1 |
| TF3 | 1 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I follow "Question 3 cannot be attempted until the previous question 2 has been completed Click to change"
Then "Question 2 cannot be attempted until the previous question 1 has been completed Click to change" "link" should be visible
And "No restriction on when question 3 can be attempted Click to change" "link" should be visible
@javascript
Scenario: Question dependency cannot apply to deferred feedback quizzes so UI is hidden
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | deferredfeedback |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
| Test questions | random | Random (Test questions) | 0 |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| Random (Test questions) | 1 | 0 |
| TF1 | 1 | 1 |
| TF2 | 1 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "be attempted" "link" in the "TF1" "list_item" should not be visible
Then "be attempted" "link" in the "TF2" "list_item" should not be visible
@javascript
Scenario: Question dependency cannot apply to questions in a shuffled section so UI is hidden
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | questionsperpage |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | 2 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 1 | 1 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "be attempted" "link" in the "TF2" "list_item" should not be visible
@javascript
Scenario: Question dependency cannot apply to the first questions in section when the previous section is shuffled
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | questionsperpage |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | 2 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 1 | 1 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 1 |
| Section 2 | 2 | 0 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "be attempted" "link" in the "TF2" "list_item" should not be visible
@javascript
Scenario: Question dependency cannot apply to quizzes with sequential navigation so UI is hidden
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | navmethod |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | sequential |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 1 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "be attempted" "link" in the "TF2" "list_item" should not be visible
@javascript
Scenario: A question can never depend on an essay
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | essay | Story | First question |
| Test questions | truefalse | TF1 | First question |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| Story | 1 | 0 |
| TF1 | 1 | 0 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "be attempted" "link" in the "TF1" "list_item" should not be visible
@javascript
Scenario: A question can never depend on a description
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | description | Info | Read me |
| Test questions | truefalse | TF1 | First question |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| Info | 1 | 0 |
| TF1 | 1 | 0 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
Then "be attempted" "link" in the "TF1" "list_item" should not be visible
@javascript
Scenario: When questions are reordered, the dependency icons are updated correctly
Given the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback |
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 quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 0 |
| TF2 | 1 | 1 |
| TF3 | 1 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I move "TF1" to "After Question 3" in the quiz by clicking the move icon
Then "Question 2 cannot be attempted until the previous question 1 has been completed Click to change" "link" should be visible
And "No restriction on when question 3 can be attempted Click to change" "link" should be visible
And "be attempted" "link" in the "TF2" "list_item" should not be visible
@@ -0,0 +1,400 @@
@mod @mod_quiz
Feature: Edit quiz page - section headings
In order to build a quiz laid out in sections the way I want
As a teacher
I need to be able to add, edit and remove section headings as well as shuffle
questions within a section.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And I log in as "teacher1"
@javascript
Scenario: We have a quiz with one default section
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
Then I should see "Shuffle"
@javascript
Scenario: Modify the default section headings
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I change quiz section heading "" to "This is section one"
Then I should see "This is section one"
@javascript
Scenario: Modify section headings
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
| Test questions | truefalse | TF4 | This is question 04 |
| Test questions | truefalse | TF5 | This is question 05 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
| TF4 | 3 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| | 1 | 0 |
| Heading 2 | 2 | 0 |
| Heading 3 | 3 | 1 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I change quiz section heading "" to "This is section one"
And I change quiz section heading "Heading 2" to "This is section two"
Then I should see "This is section one"
And I should see "This is section two"
@javascript
Scenario: Set section headings to blanks
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
| Test questions | truefalse | TF4 | This is question 04 |
| Test questions | truefalse | TF5 | This is question 05 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
| TF4 | 3 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Heading 1 | 1 | 0 |
| Heading 2 | 2 | 0 |
| Heading 3 | 3 | 1 |
And I am on the "Quiz 1" "mod_quiz > Edit" page
When I change quiz section heading "Heading 1" to ""
Then I should not see "Heading 1"
And I should see "Heading 2"
And I should see "Heading 3"
And I change quiz section heading "Heading 2" to ""
And I should not see "Heading 1"
And I should not see "Heading 2"
And I should see "Heading 3"
And I change quiz section heading "Heading 3" to ""
And I should not see "Heading 1"
And I should not see "Heading 2"
And I should not see "Heading 3"
@javascript
Scenario: Remove a section
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Heading 1 | 1 | 0 |
| Heading 2 | 2 | 0 |
| Heading 3 | 3 | 1 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I follow "Remove heading 'Heading 2'"
And I should see "Are you sure you want to remove the 'Heading 2' section heading?"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I wait until the page is ready
And I wait until "Heading 2" "text" does not exist
Then I should see "Heading 1"
And I should not see "Heading 2"
And I should see "Heading 3"
@javascript
Scenario: The edit-icon tool-tips are updated when a section is edited
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Heading 1 | 1 | 0 |
| Heading 2 | 2 | 0 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I change quiz section heading "Heading 2" to "Edited heading"
Then I should see "Edited heading"
And "Edit heading 'Edited heading'" "link" should be visible
And "Remove heading 'Edited heading'" "link" should be visible
@javascript
Scenario: Moving a question up from section 3 to the first section.
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
| Test questions | truefalse | TF4 | This is question 04 |
| Test questions | truefalse | TF5 | This is question 05 |
| Test questions | truefalse | TF6 | This is question 06 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
| TF4 | 4 |
| TF5 | 5 |
| TF6 | 6 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Heading 1 | 1 | 0 |
| Heading 2 | 3 | 0 |
| Heading 3 | 5 | 1 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I move "TF5" to "After Question 2" in the quiz by clicking the move icon
Then I should see "TF5" on quiz page "2"
@javascript
Scenario: Moving a question down from the first section to the second section.
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
| Test questions | truefalse | TF4 | This is question 04 |
| Test questions | truefalse | TF5 | This is question 05 |
| Test questions | truefalse | TF6 | This is question 06 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
| TF4 | 4 |
| TF5 | 5 |
| TF6 | 6 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Heading 1 | 1 | 0 |
| Heading 2 | 3 | 0 |
| Heading 3 | 5 | 1 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I move "TF1" to "After Question 3" in the quiz by clicking the move icon
Then I should see "TF1" on quiz page "2"
@javascript
Scenario: I should not see a delete icon for the first section in the quiz.
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Heading 1 | 1 | 0 |
| Heading 2 | 2 | 0 |
| Heading 3 | 3 | 1 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
Then "Remove heading 'Heading 1'" "link" should not exist
And "Remove heading 'Heading 2'" "link" should exist
And "Remove heading 'Heading 3'" "link" should exist
@javascript
Scenario: Turn shuffling on for a section
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Heading 1 | 1 | 0 |
| Heading 2 | 2 | 0 |
| Heading 3 | 3 | 0 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I click on shuffle for section "Heading 1" on the quiz edit page
And I click on shuffle for section "Heading 2" on the quiz edit page
Then shuffle for section "Heading 1" should be "On" on the quiz edit page
And shuffle for section "Heading 2" should be "On" on the quiz edit page
@javascript
Scenario: Turn shuffling off for a section
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Heading 1 | 1 | 1 |
| Heading 2 | 2 | 1 |
| Heading 3 | 3 | 1 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I click on shuffle for section "Heading 1" on the quiz edit page
And I click on shuffle for section "Heading 2" on the quiz edit page
Then shuffle for section "Heading 1" should be "Off" on the quiz edit page
And shuffle for section "Heading 2" should be "Off" on the quiz edit page
And I reload the page
And shuffle for section "Heading 1" should be "Off" on the quiz edit page
And shuffle for section "Heading 2" should be "Off" on the quiz edit page
@javascript
Scenario: Add section heading option only appears for pages that are not the first in their section.
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 1 |
| TF3 | 2 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I click on the "Add" page break icon after question "TF1"
And I open the action menu in "Page 1" "list_item"
Then "a new section heading" "link" in the "Page 1" "list_item" should not be visible
# Click away to close the menu.
And I click on ".numberofquestions" "css_element"
And I open the action menu in "Page 2" "list_item"
And "a new section heading" "link" in the "Page 2" "list_item" should be visible
And I click on ".numberofquestions" "css_element"
And I open the action menu in "Page 3" "list_item"
And "a new section heading" "link" in the "Page 3" "list_item" should be visible
And I click on ".numberofquestions" "css_element"
And I open the action menu in ".last-add-menu" "css_element"
And "a new section heading" "link" in the ".last-add-menu" "css_element" should not be visible
@javascript
Scenario: Verify sections are added in the right place afte ajax changes
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | This is question 01 |
| Test questions | truefalse | TF2 | This is question 02 |
| Test questions | truefalse | TF3 | This is question 03 |
| Test questions | truefalse | TF4 | This is question 04 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
| TF4 | 4 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I click on the "Remove" page break icon after question "TF1"
And I open the "Page 2" add to quiz menu
And I choose "a new section heading" in the open action menu
Then "TF3" "list_item" should exist in the "New heading" "list_item"
@javascript
Scenario: Add section works after removing a page break with more than 10 pages
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | Question 1 |
| Test questions | truefalse | TF2 | Question 2 |
| Test questions | truefalse | TF3 | Question 3 |
| Test questions | truefalse | TF4 | Question 4 |
| Test questions | truefalse | TF5 | Question 5 |
| Test questions | truefalse | TF6 | Question 6 |
| Test questions | truefalse | TF7 | Question 7 |
| Test questions | truefalse | TF8 | Question 8 |
| Test questions | truefalse | TF9 | Question 9 |
| Test questions | truefalse | TF10 | Question 10 |
| Test questions | truefalse | TF11 | Question 11 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
| TF2 | 2 |
| TF3 | 3 |
| TF4 | 4 |
| TF5 | 5 |
| TF6 | 6 |
| TF7 | 7 |
| TF8 | 8 |
| TF9 | 9 |
| TF10 | 10 |
| TF11 | 11 |
When I am on the "Quiz 1" "mod_quiz > Edit" page
And I click on the "Remove" page break icon after question "TF10"
And I open the "Page 10" add to quiz menu
And I choose "a new section heading" in the open action menu
Then "TF10" "list_item" should exist in the "New heading" "list_item"
@@ -0,0 +1,81 @@
@mod @mod_quiz
Feature: Edit quiz marks with no attempts
In order to create a quiz that awards marks the way I want
As a teacher
I must be able to set the marks I want on the Edit quiz page.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | course | idnumber | grade | decimalpoints | questiondecimalpoints |
| quiz | Quiz 1 | C1 | quiz1 | 20 | 2 | -1 |
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 | First question | Answer me |
| Test questions | truefalse | Second question | Answer again |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| First question | 1 | 2.0 |
| Second question | 1 | 3.0 |
And I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
And I change window size to "large"
@javascript
Scenario: Set the max mark for a question.
When I set the max mark for question "First question" to "7.0"
Then I should see "7.00"
And I should see "3.00"
And I should see "Total of marks: 10.00"
When I follow "Edit maximum mark"
And I press the escape key
Then I should see "7.00"
And I should see "3.00"
And I should see "Total of marks: 10.00"
And "li input[name=maxmark]" "css_element" should not exist
@javascript
Scenario: Set the overall Maximum grade.
When I set the field "maxgrade" to "10.0"
And I press "savechanges"
Then the field "maxgrade" matches value "10.00"
And I should see "2.00"
And I should see "3.00"
And I should see "Total of marks: 5.00"
@javascript
Scenario: Verify the number of decimal places shown is what the quiz settings say it should be.
# Then the field "maxgrade" matches value "20.00" -- with exact match on decimal places.
Then "//input[@name = 'maxgrade' and @value = '20.00']" "xpath_element" should exist
And I should see "2.00"
And I should see "3.00"
And I should see "Total of marks: 5.00"
And I should not see "2.000"
And I should not see "3.000"
And I should not see "Total of marks: 5.000"
And I am on the "Quiz 1" "quiz activity editing" page
When I set the following fields to these values:
| Decimal places in grades | 3 |
| Decimal places in marks for questions | 5 |
And I press "Save and display"
When I am on the "Quiz 1" "mod_quiz > Edit" page
# Then the field "maxgrade" matches value "20.000" -- with exact match on decimal places.
Then "//input[@name = 'maxgrade' and @value = '20.000']" "xpath_element" should exist
And I should see "2.00000"
And I should see "3.00000"
And I should see "Total of marks: 5.000"
And I should not see "2.000000"
And I should not see "3.000000"
And I should not see "Total of marks: 5.0000"
@@ -0,0 +1,85 @@
@mod @mod_quiz
Feature: Edit quiz marks with attempts
In order to create a quiz that awards marks the way I want
As a teacher
I must be able to set the marks I want on the Edit quiz page (even after the quiz has been attempted).
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
| student1 | S1 | Student1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | name | course | idnumber | grade | decimalpoints | questiondecimalpoints |
| quiz | Quiz 1 | C1 | quiz1 | 20 | 2 | -1 |
And I log in as "teacher1"
And I add a "True/False" question to the "Quiz 1" quiz with:
| Question name | First question |
| Question text | Answer me |
| Default mark | 2.0 |
And I add a "True/False" question to the "Quiz 1" quiz with:
| Question name | Second question |
| Question text | Answer again |
| Default mark | 3.0 |
And I log out
And I am on the "Quiz 1" "mod_quiz > View" page logged in as "student1"
And I press "Attempt quiz"
And I log out
And I log in as "teacher1"
And I am on the "Quiz 1" "mod_quiz > Edit" page
@javascript
Scenario: Set the max mark for a question.
When I set the max mark for question "First question" to "7.0"
Then I should see "7.00"
And I should see "3.00"
And I should see "Total of marks: 10.00"
When I follow "Edit maximum mark"
And I press the escape key
Then I should see "7.00"
And I should see "3.00"
And I should see "Total of marks: 10.00"
And "li input[name=maxmark]" "css_element" should not exist
@javascript
Scenario: Set the overall Maximum grade.
When I set the field "maxgrade" to "10.0"
And I press "savechanges"
Then the field "maxgrade" matches value "10.00"
And I should see "2.00"
And I should see "3.00"
And I should see "Total of marks: 5.00"
@javascript
Scenario: Verify the number of decimal places shown is what the quiz settings say it should be.
Given I change window size to "large"
# Then the field "maxgrade" matches value "20.00" -- with exact match on decimal places.
And "//input[@name = 'maxgrade' and @value = '20.00']" "xpath_element" should exist
And I should see "2.00"
And I should see "3.00"
And I should see "Total of marks: 5.00"
And I should not see "2.000"
And I should not see "3.000"
And I should not see "Total of marks: 5.000"
When I am on the "Quiz 1" "quiz activity editing" page
And I set the following fields to these values:
| Decimal places in grades | 3 |
| Decimal places in marks for questions | 5 |
And I press "Save and display"
And I am on the "Quiz 1" "mod_quiz > Edit" page
# Then the field "maxgrade" matches value "20.000" -- with exact match on decimal places.
Then "//input[@name = 'maxgrade' and @value = '20.000']" "xpath_element" should exist
And I should see "2.00000"
And I should see "3.00000"
And I should see "Total of marks: 5.000"
And I should not see "2.000000"
And I should not see "3.000000"
And I should not see "Total of marks: 5.0000"
@@ -0,0 +1,45 @@
@mod @mod_quiz @quiz
Feature: Teachers see correct answers when overriding marks
As a teacher
In order to correct errors
I must be able to override the grades that Moodle gives.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student0@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext | generalfeedback |
| Test questions | shortanswer | Kermit | What kind of animal is Kermit? | |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Kermit | 1 |
And I am on the "Quiz 1" "mod_quiz > View" page logged in as "student1"
And I press "Attempt quiz"
And I set the field "Answer" to "lion"
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"
And I log out
@javascript @_switch_window @_bug_phantomjs
Scenario: Override marking of a short answer question attempt.
When I am on the "Quiz 1 > student1 > Attempt 1" "mod_quiz > Attempt review" page logged in as "teacher1"
And I follow "Make comment or override mark"
And I switch to "commentquestion" window
Then I should see "The correct answer is: frog"
@@ -0,0 +1,75 @@
@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"
When I click on "Flag question" "button" in the "First question" "question"
Then I should see "Remove flag" in the "First question" "question"
# Confirm question 1 is flagged in navigation
And "Question 1 This page Flagged" "link" should exist
# 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 click on "Flagged" "button" in the "Second question" "question"
And I should see "Remove flag" in the "Second question" "question"
And I should see "Flag question" in the "Third 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"
And I am on the "Quiz 1 > student1 > Attempt 1" "mod_quiz > Attempt review" page
And I should see "Remove flag" in the "First question" "question"
And I should see "Remove flag" in the "Second question" "question"
And I should see "Flag question" in the "Third question" "question"
+76
View File
@@ -0,0 +1,76 @@
@mod @mod_quiz
Feature: Viewing results by group
In order to view quiz results on a large course
As a teacher
I need to filter results by group
Background:
And the following "courses" exist:
| fullname | shortname |
| Test Course 1 | C1 |
And the following "groups" exist:
| name | course | idnumber | participation |
| Group 1 | C1 | G1 | 1 |
| Group 2 | C1 | G2 | 1 |
| Group 3 | C1 | G3 | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | TeacherG1 | 1 | teacher1@example.com |
| noneditor1 | NoneditorG1 | 1 | noneditor1@example.com |
| noneditor2 | NoneditorNone | 2 | noneditor2@example.com |
| user1 | User1G1 | 1 | user1@example.com |
| user2 | User2G2 | 2 | user2@example.com |
| user3 | User3None | 3 | user3@example.com |
| user4 | User4NPgroup | 4 | user4@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| noneditor1 | C1 | teacher |
| noneditor2 | C1 | teacher |
| user1 | C1 | student |
| user2 | C1 | student |
| user3 | C1 | student |
| user4 | C1 | student |
And the following "group members" exist:
| user | group |
| teacher1 | G1 |
| noneditor1 | G1 |
| user1 | G1 |
| user2 | G2 |
| user4 | G3 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | groupmode |
| quiz | Separate quiz | quiz with separate groups | C1 | quiz1 | 1 |
| quiz | Visible quiz | quiz with visible groups | C1 | quiz2 | 2 |
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 |
And quiz "Separate quiz" contains the following questions:
| question | page |
| TF1 | 1 |
And quiz "Visible quiz" contains the following questions:
| question | page |
| TF1 | 1 |
Scenario Outline: Editing teachers should see all groups on the Results page. Non-editing teachers should see just their own
groups in Separate groups mode, all groups in Visible groups mode.
Given I am on the "<quiz>" "quiz activity" page logged in as "<user>"
And I follow "Results"
Then I <all> "All participants"
And I <G1> "Group 1"
And I <G2> "Group 2"
And I <error> "Sorry, but you need to be part of a group to see this page."
And I should not see "Group 3"
Examples:
| quiz | user | all | G1 | G2 | error |
| quiz1 | teacher1 | should see | should see | should see | should not see |
| quiz1 | noneditor1 | should not see | should see | should not see | should not see |
| quiz1 | noneditor2 | should see | should not see | should not see | should see |
| quiz2 | teacher1 | should see | should see | should see | should not see |
| quiz2 | noneditor1 | should see | should see | should see | should not see |
| quiz2 | noneditor2 | should see | should see | should see | should not see |
+55
View File
@@ -0,0 +1,55 @@
@mod @mod_quiz
Feature: Display of information before starting a quiz
As a student
In order to start a quiz with confidence
I need information about the quiz settings before I start an attempt
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student | Student | One | student@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
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 "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | Text of the first question |
Scenario: Check the pass grade is displayed
Given the following "activities" exist:
| activity | name | intro | course | idnumber | gradepass |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 60.00 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
Then I should see "Grade to pass: 60.00 out of 100.00"
Scenario: Check the pass grade is displayed with custom decimal separator
Given the following "language customisations" exist:
| component | stringid | value |
| core_langconfig | decsep | # |
And the following "activities" exist:
| activity | name | intro | course | idnumber | gradepass |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 60#00 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
Then I should see "Grade to pass: 60#00 out of 100#00"
Scenario: Check the pass grade is not displayed if not set
Given the following "activities" exist:
| activity | name | intro | course | idnumber | gradepass |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
Then I should not see "Grade to pass: 0.00"
@@ -0,0 +1,79 @@
@mod @mod_quiz
Feature: Teachers can override the grade for any question
As a teacher
In order to correct errors
I must be able to override the grades that Moodle gives.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student0@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext | defaultmark |
| Test questions | essay | TF1 | First question | 20 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | grade |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 20 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
And the following "user private files" exist:
| user | filepath |
| teacher1 | mod/quiz/tests/fixtures/moodle_logo.jpg |
And I am on the "Quiz 1" "mod_quiz > View" page logged in as "student1"
And I press "Attempt quiz"
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"
And I log out
@javascript @_switch_window @_bug_phantomjs
Scenario: Validating the marking of an essay question attempt.
When I am on the "Quiz 1 > student1 > Attempt 1" "mod_quiz > Attempt review" page logged in as "teacher1"
And I follow "Make comment or override mark"
And I switch to "commentquestion" window
And I set the field "Mark" to "25"
And I press "Save"
Then I should see "This grade is outside the valid range."
And I set the field "Mark" to "aa"
And I press "Save"
And I should see "That is not a valid number."
And I set the field "Mark" to "10.0"
And I press "Save" and switch to main window
And I should see "Complete" in the "Manually graded 10 with comment: " "table_row"
And I follow "Make comment or override mark"
And I switch to "commentquestion" window
And I should see "Teacher 1" in the "Manually graded 10 with comment: " "table_row"
@javascript @_switch_window @_file_upload @_bug_phantomjs @editor_tiny
Scenario: Comment on a response to an essay question attempt.
When I log in as "teacher1"
And I am on the "Quiz 1 > student1 > Attempt 1" "mod_quiz > Attempt review" page
And I follow "Make comment or override mark"
And I switch to "commentquestion" window
And I set the field "Comment" to "Administrator's comment"
And I select the "p" element in position "0" of the "Comment" TinyMCE editor
And I click on the "Image" button for the "Comment" TinyMCE editor
And I click on "Browse repositories" "button" in the "Insert image" "dialogue"
And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
And I click on "moodle_logo.jpg" "link"
And I click on "Select this file" "button"
And I set the field "How would you describe this image to someone who can't see it?" to "It's the logo"
And I click on "Save" "button" in the "Image details" "dialogue"
And I press "Save" and switch to main window
And I switch to the main window
Then I should see "Commented: [It's the logo]" in the ".history table" "css_element"
And "//img[contains(@src, 'moodle_logo.jpg')]" "xpath_element" should exist in the ".comment" "css_element"
# This time is same as time the window is open. So wait for it to close before proceeding.
And I wait "2" seconds
+115
View File
@@ -0,0 +1,115 @@
@mod @mod_quiz
Feature: Preview a quiz as a teacher
In order to verify my quizzes are ready for my students
As a teacher
I need to be able to preview them
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher | Teacher | One | teacher@example.com |
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 "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | |
| TF2 | 1 | 3.0 |
And user "teacher" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | False |
@javascript
Scenario: Review the quiz attempt
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "teacher"
And I follow "Review"
Then I should see "25.00 out of 100.00"
And I should see "v1 (latest)" in the "Question 1" "question"
And I follow "Finish review"
And "Review" "link" in the "Attempt 1" "list_item" should be visible
@javascript
Scenario: Review the quiz attempt with custom decimal separator
Given the following "language customisations" exist:
| component | stringid | value |
| core_langconfig | decsep | # |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "teacher"
And I follow "Review"
Then I should see "1#00/4#00"
And I should see "25#00 out of 100#00"
And I should see "Mark 1#00 out of 1#00"
And I follow "Finish review"
And "Review" "link" in the "Attempt 1" "list_item" should be visible
Scenario: Preview the quiz
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "teacher"
When I press "Preview quiz"
Then I should see "Question 1"
And I should see "v1 (latest)" in the "Question 1" "question"
And "Start a new preview" "button" should exist
Scenario: Teachers should see a notice if the quiz is not available to students
Given the following "activities" exist:
| activity | name | course | timeclose |
| quiz | Quiz 2 | C1 | ##yesterday## |
And quiz "Quiz 2" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | |
| TF2 | 1 | 3.0 |
When I am on the "Quiz 2" "mod_quiz > View" page logged in as "admin"
And I should see "This quiz is currently not available."
And I press "Preview quiz"
Then I should see "if this were a real attempt, you would be blocked" in the ".alert-warning" "css_element"
Scenario: Admins should be able to preview a quiz
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "admin"
When I press "Preview quiz"
Then I should see "Question 1"
And "Start a new preview" "button" should exist
@javascript
Scenario: Teacher responses should be cleared after updating the question too much in the preview.
Given the following "activities" exist:
| activity | name | course |
| quiz | Quiz 3 | C1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | multichoice | Multi-choice-002 | one_of_four |
And quiz "Quiz 3" contains the following questions:
| question | page |
| Multi-choice-002 | 1 |
When I am on the "Quiz 3" "mod_quiz > View" page logged in as "teacher"
And I press "Preview quiz"
And I should see "one_of_four"
And I should see "v1 (latest)"
And I click on "One" "qtype_multichoice > Answer"
And I click on "Two" "qtype_multichoice > Answer"
And I press "Finish attempt ..."
And I press "Return to attempt"
And I click on "Edit question" "link" in the "Question 1" "question"
And I set the field "Question text" to "one_of_four version 2"
And I set the field "Choice 4" to ""
And I press "id_submitbutton"
Then I should see "one_of_four version 2"
And I should see "v2 (latest)"
And I should see "One"
And I should see "Two"
And I should see "Three"
And I should not see "Four"
And "input[type=checkbox][name$=choice0]:checked" "css_element" should not exist
And "input[type=checkbox][name$=choice1]:checked" "css_element" should not exist
And "input[type=checkbox][name$=choice2]:checked" "css_element" should not exist
@@ -0,0 +1,132 @@
@mod @mod_quiz
Feature: Quiz availability can be set
In order to see quiz availability
As a teacher
I need to be able to set quiz opening and closing times
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | One | teacher1@example.com |
| student1 | Student | One | student1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
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 |
Scenario Outline: Set quiz opening time while closing time is disabled
Given the following "activities" exist:
| activity | course | name | timeopen |
| quiz | C1 | Quiz 1 | <timeopen> |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | 2 |
When I am on the "Quiz 1" "quiz activity" page logged in as student1
# Confirm display as student depending on case.
Then I should see "<opentext>:"
And I should see "<timeopen>%A, %d %B %Y, %I:%M##"
And I should not see "Close:"
And I <quizavailability> see "This quiz is currently not available."
And "Attempt quiz" "button" <attemptvisibility> exist
Examples:
| opentext | timeopen | attemptvisibility | quizavailability |
# Case 1 - open is set to future date, close is disabled.
| Opens | ##tomorrow## | should not | should |
# Case 4 - open is set to past date, close is disabled.
| Opened | ##yesterday## | should | should not |
Scenario Outline: Set quiz closing time while opening time is disabled
Given the following "activities" exist:
| activity | course | name | timeclose |
| quiz | C1 | Quiz 1 | <timeclose> |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | 2 |
When I am on the "Quiz 1" "quiz activity" page logged in as student1
# Confirm display as student depending on case.
Then I should see "<closetext>:"
And I should see "<timeclose>%A, %d %B %Y, %I:%M##"
And I <quizavailability> see "This quiz is currently not available."
And "Attempt quiz" "button" <attemptvisibility> exist
Examples:
| closetext | timeclose | attemptvisibility | quizavailability |
# Case 2 - open is disabled, close is set to past date.
| Closed | ##yesterday## | should not | should not |
# Case 5 - open is disabled, close is set to future date.
| Closes | ##tomorrow## | should | should not |
Scenario Outline: Set quiz opening and closing times
Given the following "activities" exist:
| activity | course | name | timeopen | timeclose |
| quiz | C1 | Quiz 1 | <timeopen> | <timeclose> |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | 2 |
When I am on the "Quiz 1" "quiz activity" page logged in as student1
# Confirm display as student depending on case.
Then I should see "<opentext>:"
And I should see "<timeopen>%A, %d %B %Y, %I:%M##"
And I should see "<closetext>:"
And I should see "<timeclose>%A, %d %B %Y, %I:%M##"
And I <quizavailability> see "This quiz is currently not available."
And "Attempt quiz" "button" <attemptvisibility> exist
Examples:
| opentext | timeopen | closetext | timeclose | attemptvisibility | quizavailability |
# Case 6 - open and close are set to past date.
| Opened | ##3 days ago## | Closed | ##yesterday## | should not | should not |
# Case 7 - open is set to past date, close is set to future date.
| Opened | ##yesterday## | Closes | ##tomorrow## | should | should not |
# Case 8 - open and close are set to future date
| Opens | ##tomorrow## | Closes | ##+2 days## | should not | should |
Scenario: Quiz time open and time close are disabled
# Case 3 - both open and close are disabled.
Given the following "activities" exist:
| activity | course | name |
| quiz | C1 | Quiz 1 |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | 2 |
When I am on the "Quiz 1" "quiz activity" page logged in as student1
Then I should not see "Opens"
And I should not see "Opened"
And I should not see "Closes"
And I should not see "Closed"
And I should not see "This quiz is currently not available."
And "Attempt quiz" "button" should exist
@javascript
Scenario Outline: Timer is displayed when quiz closes in less than an hour
Given the following "activities" exist:
| activity | course | name | timeclose |
| quiz | C1 | Quiz 1 | <closedate> |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
And I am on the "Quiz 1" "quiz activity" page logged in as "teacher1"
When I press "Preview quiz"
# Confirm timer visibility for teacher
Then I <timervisibility> see "Time left"
And I am on the "Quiz 1" "quiz activity" page logged in as "student1"
And I press "Attempt quiz"
# Confirm timer visibility for student
And I <timervisibility> see "Time left"
Examples:
| closedate | timervisibility |
# Case 1 - closedate is < 1hr, the timer is visible
| ##now +10 minutes## | should |
# Case 2 - closedate is > 1hr, the timer is not visible
| ##now +2 hours## | should not |
@@ -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"
@@ -0,0 +1,83 @@
@mod @mod_quiz @core_completion
Feature: View activity completion in the quiz activity
In order to have visibility of quiz completion requirements
As a student
I need to be able to view my quiz completion progress
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
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 | First question | Answer the first question |
And the following "activity" exists:
| activity | quiz |
| course | C1 |
| idnumber | quiz1 |
| name | Test quiz name |
| section | 1 |
| attempts | 2 |
| gradepass | 5.00 |
| completion | 2 |
| completionview | 1 |
| completionusegrade | 1 |
| completionpassgrade | 1 |
| completionattemptsexhausted | 1 |
| completionminattemptsenabled | 1 |
| completionminattempts | 1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
Scenario Outline: View automatic completion items as a student
When I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test quiz name"
And the "View" completion condition of "Test quiz name" is displayed as "done"
And the "Make attempts: 1" completion condition of "Test quiz name" is displayed as "todo"
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "todo"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "todo"
And the "Receive a pass grade or complete all available attempts" completion condition of "Test quiz name" is displayed as "todo"
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | False |
And I am on "Course 1" course homepage
And I follow "Test quiz name"
And the "View" completion condition of "Test quiz name" is displayed as "done"
And the "Make attempts: 1" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "failed"
And the "Receive a pass grade or complete all available attempts" completion condition of "Test quiz name" is displayed as "todo"
And I press "Re-attempt quiz"
And I set the field "<answer>" to "1"
And I press "Finish attempt ..."
And I press "Submit all and finish"
And I follow "Finish review"
And the "View" completion condition of "Test quiz name" is displayed as "done"
And the "Make attempts: 1" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "<passcompletionexpected>"
And the "Receive a pass grade or complete all available attempts" completion condition of "Test quiz name" is displayed as "done"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Reports" in current page administration
And I click on "Activity completion" "link"
And "<expectedactivitycompletion>" "icon" should exist in the "Student 1" "table_row"
Examples:
| answer | passcompletionexpected | expectedactivitycompletion |
| False | failed | Completed (did not achieve pass grade) |
| True | done | Completed (achieved pass grade) |
@@ -0,0 +1,54 @@
@mod @mod_quiz @core_completion
Feature: Ensure saving a quiz does not modify the completion settings.
In order to reliably use completion
As a teacher
I need to be able to update the quiz
without changing the completion settings.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
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 | First question | Answer the first question |
And the following "activity" exists:
| activity | quiz |
| course | C1 |
| idnumber | quiz1 |
| name | Test quiz |
| section | 1 |
| attempts | 2 |
| gradepass | 5.00 |
| completion | 2 |
| completionview | 0 |
| completionusegrade | 1 |
| completionpassgrade | 1 |
| completionattemptsexhausted | 1 |
And quiz "Test quiz" contains the following questions:
| question | page |
| First question | 1 |
And user "student1" has attempted "Test quiz" with responses:
| slot | response |
| 1 | True |
Scenario: Ensure saving quiz activty does not change completion settings
Given I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher1"
When I navigate to "Settings" in current page administration
Then the "completionattemptsexhausted" "field" should be disabled
And the field "completionattemptsexhausted" matches value "1"
And I press "Save and display"
And I navigate to "Settings" in current page administration
And the "completionattemptsexhausted" "field" should be disabled
And the field "completionattemptsexhausted" matches value "1"
@@ -0,0 +1,72 @@
@mod @mod_quiz @core_completion
Feature: Activity completion in the quiz activity with unlocked and re-grading.
In order to have visibility of quiz completion requirements
As a student
I need to be able to view my quiz completion progress even teacher have re-grading the grade pass.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext | defaultmark |
| Test questions | truefalse | First question | Answer the first question | 8 |
| Test questions | truefalse | Second question | Answer the second question | 2 |
And the following "activity" exists:
| activity | quiz |
| course | C1 |
| idnumber | quiz1 |
| name | Test quiz name |
| section | 1 |
| gradepass | 8 |
| grade | 10 |
| grademethod | 1 |
| completion | 2 |
| completionusegrade | 1 |
| completionpassgrade | 1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
| Second question | 2 |
@javascript
Scenario: Student will receive correct completion even when teacher unlocked completion and re-grading.
Given I am on the "Test quiz name" "quiz activity" page logged in as student1
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "todo"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "todo"
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | True |
| 2 | False |
And I am on "Course 1" course homepage
And I follow "Test quiz name"
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "done"
And I log out
When I am on the "Course 1" course page logged in as teacher1
And I navigate to "Reports > Activity completion" in current page administration
And "Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row"
And I am on the "Test quiz name" "quiz activity" page
And I navigate to "Settings" in current page administration
And I expand all fieldsets
And I press "Unlock completion settings"
And I set the following fields to these values:
| gradepass | 10 |
And I press "Save and return to course"
And I navigate to "Reports > Activity completion" in current page administration
Then "Completed (achieved pass grade)" "icon" should not exist in the "Student 1" "table_row"
And I log out
And I am on the "Test quiz name" "quiz activity" page logged in as student1
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "failed"
@@ -0,0 +1,82 @@
@mod @mod_quiz
Feature: Enable deferred or immediate feedback for quiz
As a teacher
I should be able to set how questions behave to deferred or immediate feedback
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | 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 |
@javascript
Scenario: Attempt quiz with How questions behave set to Deferred Feedback
Given the following "activity" exists:
| activity | quiz |
| name | Quiz 1 |
| course | C1 |
| preferredbehaviour | deferredfeedback |
| 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 |
And I am on the "Quiz 1" "quiz activity" page logged in as student1
When I press "Attempt quiz"
# Confirm that check button does not exist when attempting quiz
Then "Check Question 1" "button" should not exist
And I set the field "False" to "1"
And I press "Finish attempt ..."
And I should not see "This is the wrong answer."
And I should not see "You should have selected true."
And I should not see "The correct answer is 'True'."
And I press "Submit all and finish"
# Confirm that quiz answer feedback only appears when attempt is submitted
And I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue"
And I should see "This is the wrong answer."
And I should see "You should have selected true."
And I should see "The correct answer is 'True'."
Scenario: Attempt quiz with How questions behave set to Immediate Feedback
Given the following "activity" exists:
| activity | quiz |
| name | Quiz 1 |
| course | C1 |
| preferredbehaviour | immediatefeedback |
| correctnessduring | 1 |
| marksduring | 1 |
| specificfeedbackduring | 1 |
| generalfeedbackduring | 1 |
| rightanswerduring | 1 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
And I am on the "Quiz 1" "quiz activity" page logged in as student1
When I press "Attempt quiz"
Then "Check Question 1" "button" should exist
And I set the field "False" to "1"
# Confirm you can check your answer immediately before submitting the attempt
And I press "Check"
And I should see "The correct answer is 'True'."
And the "True" "field" should be disabled
And the "False" "field" should be disabled
And "Check Question 1" "button" should not exist
@@ -0,0 +1,139 @@
@mod @mod_quiz
Feature: Quiz group override
In order to grant a group special access to a quiz
As a teacher
I need to create an override for that group.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Terry 1 | Teacher 1 | teacher1@example.com |
| student1 | Sam 1 | Student 1 | student1@example.com |
| teacher2 | Terry 2 | Teacher 2 | teacher2@example.com |
| student2 | Sam 2 | Student 2 | student2@example.com |
| teacher3 | Terry 3 | Teacher 3 | teacher3@example.com |
| student3 | Sam 3 | Student 3 | student3@example.com |
| helper | Exam | Helper | helper@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| teacher2 | C1 | editingteacher |
| student2 | C1 | student |
| teacher3 | C1 | editingteacher |
| student3 | C1 | student |
| helper | C1 | teacher |
And the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | G1 |
| Group 2 | C1 | G2 |
| Group 3 | C1 | G3 |
And the following "group members" exist:
| user | group |
| student1 | G1 |
| teacher1 | G1 |
| teacher1 | G3 |
| student2 | G2 |
| teacher2 | G2 |
| teacher2 | G3 |
| student3 | G3 |
| helper | G1 |
| helper | G2 |
| helper | G3 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | groupmode |
| quiz | Test quiz | Test quiz description | C1 | quiz1 | 1 |
Scenario: Override Group 1 as teacher of Group 1
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/site:accessallgroups | Prevent | editingteacher | Course | C1 |
When I am on the "Test quiz" "mod_quiz > Group overrides" page logged in as "teacher1"
And I press "Add group override"
Then the "Override group" select box should contain "Group 1"
And the "Override group" select box should not contain "Group 2"
Scenario: Add button disabled if there are no groups
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/site:accessallgroups | Prevent | editingteacher | Course | C1 |
When I am on the "Test quiz" "mod_quiz > Group overrides" page logged in as "teacher3"
Then I should see "No groups you can access."
And the "Add group override" "button" should be disabled
Scenario: A teacher can create an override
When I am on the "Test quiz" "mod_quiz > Group overrides" page logged in as "teacher1"
And I press "Add group override"
And I set the following fields to these values:
| Override group | Group 1 |
| Attempts allowed | 2 |
And I press "Save and enter another override"
And I set the following fields to these values:
| Override group | Group 3 |
| Attempts allowed | 2 |
And I press "Save"
Then "Group 1" "table_row" should exist
# Check all column headers are present.
And I should see "Group" in the "Overrides" "table_row"
And I should see "Action" in the "Overrides" "table_row"
Scenario: A teacher with accessallgroups permission should see all group overrides
Given the following "mod_quiz > group overrides" exist:
| quiz | group | attempts |
| Test quiz | G1 | 2 |
| Test quiz | G2 | 2 |
When I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher1"
Then I should see "Settings overrides exist (Groups: 2)"
And I follow "Groups: 2"
And "Group 1" "table_row" should exist
And "Group 2" "table_row" should exist
Scenario: A teacher without accessallgroups permission should only see the group overrides within his/her groups, when the activity's group mode is "separate groups"
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/site:accessallgroups | Prevent | editingteacher | Course | C1 |
And the following "mod_quiz > group overrides" exist:
| quiz | group | attempts |
| Test quiz | G1 | 2 |
| Test quiz | G2 | 2 |
When I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher1"
Then I should see "Settings overrides exist (Groups: 1) for your groups"
And I follow "Groups: 1"
Then "Group 1" "table_row" should exist
And "Group 2" "table_row" should not exist
Scenario: A non-editing teacher can see the overrides, but not change them
Given the following "mod_quiz > group overrides" exist:
| quiz | group | attempts |
| Test quiz | G1 | 2 |
| Test quiz | G2 | 2 |
When I am on the "Test quiz" "mod_quiz > Group overrides" page logged in as "helper"
Then "Group 1" "table_row" should exist
And "Group 2" "table_row" should exist
And "Add group override" "button" should not exist
And "Edit" "link" should not exist in the "Group 1" "table_row"
And "Copy" "link" should not exist in the "Group 1" "table_row"
And "Delete" "link" should not exist in the "Group 1" "table_row"
Scenario: "Not visible" groups should not be available for group overrides
Given the following "groups" exist:
| name | course | idnumber | visibility | participation |
| Visible to everyone/Participation | C1 | VP | 0 | 1 |
| Only visible to members/Participation | C1 | MP | 1 | 1 |
| Only see own membership | C1 | O | 2 | 0 |
| Not visible | C1 | N | 3 | 0 |
| Visible to everyone/Non-Participation | C1 | VN | 0 | 0 |
| Only visible to members/Non-Participation | C1 | MN | 1 | 0 |
When I am on the "quiz1" Activity page logged in as teacher1
And I navigate to "Overrides" in current page administration
And I select "Group overrides" from the "jump" singleselect
And I press "Add group override"
Then I should see "Visible to everyone/Participation" in the "Override group" "select"
And I should see "Visible to everyone/Non-Participation" in the "Override group" "select"
And I should see "Only visible to members" in the "Override group" "select"
And I should see "Only visible to members/Non-Participation" in the "Override group" "select"
And I should see "Only see own membership" in the "Override group" "select"
And I should not see "Not visible" in the "Override group" "select"
@@ -0,0 +1,75 @@
@mod @mod_quiz
Feature: Set a quiz to be interactive with multiple tries
In order to attempt an interactive quiz multiple times
As a teacher
I should be able to set how questions behave to interactive with multiple tries
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 | template | hint1 | hint2 | shuffleanswers |
| Test questions | multichoice | MC1 | First question | one_of_four | Hint 1 | Hint 2 | 0 |
| Test questions | multichoice | MC2 | Second question | one_of_four | Hint 1 | Hint 2 | 0 |
And the following "activities" exist:
| activity | name | course | preferredbehaviour | specificfeedbackduring |
| quiz | Quiz 1 | C1 | interactive | 1 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| MC1 | 1 |
| MC2 | 2 |
@javascript
Scenario: Attempt an interactive quiz with multiple tries
Given I am on the "Quiz 1" "quiz activity" page logged in as student1
And I press "Attempt quiz"
# Answer the question incorrectly.
And I click on "Two" "qtype_multichoice > Answer" in the "Question 1" "question"
When I press "Check"
# Confirm that correct feedback is displayed.
Then I should see "That is not right at all."
And I should see "Hint 1."
# Confirm answer cannot be changed after checking your answer.
And I should not see "Clear my choice"
And "Check" "button" should not be visible
# Attempt question again.
And I press "Try again"
And I should see "Clear my choice"
# Answer the question correctly.
And I click on "One" "qtype_multichoice > Answer" in the "Question 1" "question"
And I press "Check"
# Confirm correct feedback is displayed.
And I should see "Well done!"
And I should see "The correct answer is: One"
And I should not see "Hint 1."
And "Try again" "button" should not be visible
And I press "Next page"
# Answer question incorrectly.
And I click on "Two" "qtype_multichoice > Answer" in the "Question 2" "question"
And I press "Check"
# Confirm Hint 1 is displayed.
And I should see "Hint 1."
And I press "Try again"
# Answer question incorrectly again.
And I click on "Three" "qtype_multichoice > Answer" in the "Question 2" "question"
And I press "Check"
# Confirm Hint 2 is displayed.
And I should see "Hint 2."
And I press "Try again"
# Answer question incorrectly again.
And I click on "Four" "qtype_multichoice > Answer" in the "Question 2" "question"
And I press "Check"
# Confirm you can no longer re-attempt the question.
And I should not see "Try again"
@@ -0,0 +1,51 @@
@mod @mod_quiz
Feature: Quiz with no calendar capabilites
In order to allow work effectively
As a teacher
I need to be able to create quiz even when I cannot edit calendar events
Background:
Given the following "courses" exist:
| fullname | shortname | category | groupmode |
| Course 1 | C1 | 0 | 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activity" exists:
| activity | quiz |
| course | C1 |
| idnumber | 00001 |
| name | Test quiz name |
| intro | Test quiz description |
| section | 1 |
And I log in as "admin"
And I am on "Course 1" course homepage
And I follow "Test quiz name"
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| id_timeopen_enabled | 1 |
| id_timeopen_day | 1 |
| id_timeopen_month | 1 |
| id_timeopen_year | 2017 |
| id_timeclose_enabled | 1 |
| id_timeclose_day | 1 |
| id_timeclose_month | 2 |
| id_timeclose_year | 2017 |
And I press "Save and return to course"
And I am on the "Course 1" "permissions" page
And I override the system permissions of "Teacher" role with:
| capability | permission |
| moodle/calendar:manageentries | Prohibit |
And I log out
Scenario: Editing a quiz
When I am on the "Test quiz name" "mod_quiz > View" page logged in as "teacher1"
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| id_timeopen_year | 2018 |
| id_timeclose_year | 2018 |
And I press "Save and return to course"
Then I should see "Test quiz name"
@@ -0,0 +1,139 @@
@mod @mod_quiz
Feature: Quiz question versioning
In order to manage question versions
As a teacher
I need to be able to choose which versions can be displayed in a quiz
Background:
Given the following "courses" exist:
| fullname | shortname | category | groupmode |
| Course 1 | C1 | 0 | 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher | Teacher | 1 | teacher@example.com |
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 "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
| Test questions | truefalse | Other question | Answer the first question |
And quiz "Quiz 1" contains the following questions:
| question | page |
| First question | 1 |
@javascript
Scenario: Appropriate question version should be displayed when not edited
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher"
Then I should see "First question"
And I should see "Answer the first question"
And the field "version" matches value "Always latest"
And the "version" select box should contain "v1 (latest)"
# We check that the corresponding version is the appropriate one in preview
And I click on "Preview question" "link"
And I switch to "questionpreview" window
And I should see "Version 1 (latest)"
And I should see "Answer the first question"
And I click on "Submit and finish" "button"
And I should see "You should have selected true."
@javascript
Scenario: Approriate question version should be displayed when edited
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher"
And I click on "Edit question First question" "link"
# We edit the question with new informations to generate a second version
And I set the following fields to these values:
| Question name | First question (v2) |
| Question text | Answer the new first question |
| Correct answer | False |
And I press "id_submitbutton"
And the field "version" matches value "Always latest"
And the "version" select box should contain "v1"
And I set the field "version" to "v2 (latest)"
Then I should see "First question (v2)"
And I should see "Answer the new first question"
And I click on "Preview question" "link"
And I switch to "questionpreview" window
# We check that the corresponding version is the appropriate one in preview
# We also check that the new information is properly displayed
And I should see "Version 2 (latest)"
And I should see "Answer the new first question"
@javascript
Scenario: Appropriate question version displayed when later draft version exists
# Edit the question in the question bank to add a new draft version.
Given I am on the "First question" "core_question > edit" page logged in as teacher
And I set the following fields to these values:
| Question name | First question (v2) |
| Question text | Answer the new first question |
| Correct answer | False |
| Question status | Draft |
And I press "id_submitbutton"
When I am on the "Quiz 1" "mod_quiz > Edit" page
Then I should see "First question"
And I should see "Answer the first question"
And the field "version" matches value "Always latest"
And the "version" select box should contain "v1 (latest)"
And the "version" select box should not contain "v2"
And the "version" select box should not contain "v2 (latest)"
And I am on the "Quiz 1" "mod_quiz > View" page
And I press "Preview quiz"
And I should see "Answer the first question"
@javascript
Scenario: Creating a new question should have always latest in the version selection
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher"
# Change the version of the existing question, to ensure it does not match later.
And I set the field "version" to "v1 (latest)"
And I open the "Page 1" add to quiz menu
And I follow "a new question"
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
And I set the following fields to these values:
| Question name | New essay |
| Question text | Write 200 words about quizzes. |
And I press "id_submitbutton"
And I should see "New essay" on quiz page "1"
And the field "version" in the "New essay" "list_item" matches value "Always latest"
@javascript
Scenario: Adding a question from question bank should have always latest in the version selection
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher"
And I open the "Page 1" add to quiz menu
And I follow "from question bank"
And I click on "Select" "checkbox" in the "Other question" "table_row"
And I press "Add selected questions to the quiz"
Then I should see "Other question" on quiz page "1"
And the field "version" in the "Other question" "list_item" matches value "Always latest"
@javascript
Scenario: Adding a question where all available versions are drafts should display a helpful message.
Given quiz "Quiz 1" contains the following questions:
| question | page |
| First question | 1 |
And I am on the "Quiz 1" "mod_quiz > Question bank" page logged in as teacher
And I set the field "question_status_dropdown" in the "First question" "table_row" to "Draft"
When I am on the "Quiz 1" "mod_quiz > Edit" page
Then I should see "This question is in draft status. To use it in the quiz, go to the question bank and change the status to ready."
@javascript
Scenario: Previewing a question set to use always latest version will set the preview to always latest version
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher"
And the field "version" in the "First question" "list_item" matches value "Always latest"
When I follow "Preview question"
And I expand all fieldsets
Then the field "Question version" matches value "Always latest"
@javascript
Scenario: Previewing a question set to use a specific version will set the preview to that version
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher"
And I set the field "version" to "v1 (latest)"
When I follow "Preview question"
And I expand all fieldsets
Then the field "Question version" matches value "1"
+72
View File
@@ -0,0 +1,72 @@
@mod @mod_quiz
Feature: Quiz reset
In order to reuse past quizzes
As a teacher
I need to remove all previous data.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Terry1 | Teacher1 | teacher1@example.com |
| student1 | Sam1 | Student1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | G1 |
| Group 2 | C1 | G2 |
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 |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Test quiz name | Test quiz description | C1 | quiz1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| TF1 | 1 |
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | True |
Scenario: Use course reset to clear all attempt data
When I log in as "teacher1"
And I am on the "Course 1" "reset" page
And I set the following fields to these values:
| Delete all quiz attempts | 1 |
And I press "Reset course"
And I press "Continue"
And I am on the "Test quiz name" "mod_quiz > Grades report" page
Then I should see "Attempts: 0"
Scenario: Use course reset to remove user overrides.
Given the following "mod_quiz > user overrides" exist:
| quiz | user | attempts |
| Test quiz name | student1 | 2 |
When I log in as "teacher1"
And I am on the "Course 1" "reset" page
And I set the field "Delete all user overrides" to "1"
And I press "Reset course"
And I press "Continue"
And I am on the "Test quiz name" "mod_quiz > User overrides" page
Then I should not see "Sam1 Student1"
Scenario: Use course reset to remove group overrides.
Given the following "mod_quiz > group overrides" exist:
| quiz | group | attempts |
| Test quiz name | G1 | 2 |
When I log in as "teacher1"
And I am on the "Course 1" "reset" page
And I set the following fields to these values:
| Delete all group overrides | 1 |
And I press "Reset course"
And I press "Continue"
And I am on the "Test quiz name" "mod_quiz > Group overrides" page
Then I should not see "Group 1"
@@ -0,0 +1,242 @@
@mod @mod_quiz
Feature: Quiz user override
In order to grant a student special access to a quiz
As a teacher
I need to create an override for that user.
Background:
And the following "custom profile fields" exist:
| datatype | shortname | name |
| text | frog | Favourite frog |
Given the following "users" exist:
| username | firstname | lastname | email | profile_field_frog |
| teacher | Teacher | One | teacher@example.com | |
| helper | Exam | Helper | helper@example.com | |
| student1 | Student | One | student1@example.com | yellow frog |
| student2 | Student | Two | student2@example.com | prince frog |
| student3 | Student | Three | student3@example.com | Kermit |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| helper | C1 | teacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
@javascript
Scenario: Add, modify then delete a user override
Given the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher"
And I change window size to "large"
And I navigate to "Overrides" in current page administration
And I press "Add user override"
And I set the following fields to these values:
| Override user | Student One (student1@example.com) |
| id_timeclose_enabled | 1 |
| timeclose[day] | 1 |
| timeclose[month] | January |
| timeclose[year] | 2020 |
| timeclose[hour] | 08 |
| timeclose[minute] | 00 |
And I press "Save"
Then I should see "Wednesday, 1 January 2020, 8:00"
And I click on "Edit" "link" in the "Student One" "table_row"
And I set the following fields to these values:
| timeclose[year] | 2030 |
And I press "Save"
And I should see "Action"
And I should see "Tuesday, 1 January 2030, 8:00" in the "Student One" "table_row"
And I should see "student1@example.com" in the "Student One" "table_row"
And I click on "Delete" "link" in the "Student One" "table_row"
And I should see "Are you sure you want to delete the override for user Student One (student1@example.com)?"
And I press "Continue"
And I should not see "Student One"
@javascript
Scenario: Add multiple user overrides, one after another
Given the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher"
And I change window size to "large"
And I navigate to "Overrides" in current page administration
And I press "Add user override"
And I set the following fields to these values:
| Override user | Student One |
| timeclose[enabled] | 1 |
| Close the quiz | ## 1 January 2020 08:00 ## |
And I press "Save and enter another override"
And I set the following fields to these values:
| Override user | Student Two |
| timeclose[enabled] | 1 |
| Close the quiz | ## 2 January 2020 08:00 ## |
When I press "Save"
Then the following should exist in the "generaltable" table:
| User | Overrides | -4- |
| Student One | Quiz closes | 1 January 2020, 8:00 |
| Student Two | Quiz closes | 2 January 2020, 8:00 |
@javascript
Scenario: Can add a user override when the quiz is not available to the student
Given the following "activities" exist:
| activity | name | course | idnumber | visible |
| quiz | Test quiz | C1 | quiz1 | 0 |
When I am on the "Test quiz" "mod_quiz > User overrides" page logged in as "teacher"
And I press "Add user override"
And I set the following fields to these values:
| Override user | Student One (student1@example.com) |
| Attempts allowed | 1 |
And I press "Save"
Then I should see "This override is inactive"
And I should see "Action"
And "Edit" "icon" should exist in the "Student One" "table_row"
And "copy" "icon" should exist in the "Student One" "table_row"
And "Delete" "icon" should exist in the "Student One" "table_row"
And I follow "Student One"
And I should see "Student One"
And I should see "User details"
@javascript
Scenario: Teacher without 'See full user identity in lists' can see and edit overrides
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/site:viewuseridentity | Prevent | editingteacher | Course | C1 |
And the following "activities" exist:
| activity | name | course | idnumber | visible |
| quiz | Test quiz | C1 | quiz1 | 0 |
When I am on the "Test quiz" "mod_quiz > User overrides" page logged in as "teacher"
And I press "Add user override"
And I set the following fields to these values:
| Override user | Student One |
| Attempts allowed | 1 |
And I press "Save"
And I should see "Action"
And I should not see "student1@example.com"
And "Edit" "icon" should exist in the "Student One" "table_row"
And "copy" "icon" should exist in the "Student One" "table_row"
And "Delete" "icon" should exist in the "Student One" "table_row"
Scenario: A teacher without accessallgroups permission should only be able to add user override for users that he/she shares groups with,
when the activity's group mode is to "separate groups"
Given the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | G1 |
| Group 2 | C1 | G2 |
And the following "group members" exist:
| user | group |
| student1 | G1 |
| teacher | G1 |
| student2 | G2 |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/site:accessallgroups | Prevent | editingteacher | Course | C1 |
And the following "activities" exist:
| activity | name | course | idnumber | groupmode |
| quiz | Test quiz | C1 | quiz1 | 1 |
When I am on the "Test quiz" "mod_quiz > User overrides" page logged in as "teacher"
And I press "Add user override"
Then the "Override user" select box should contain "Student One (student1@example.com)"
And the "Override user" select box should not contain "Student Two (student2@example.com)"
Scenario: Override user in an activity with group mode set to "separate groups" as a teacher who is not a member in any group, and does not have accessallgroups permission
Given the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | G1 |
And the following "group members" exist:
| user | group |
| student1 | G1 |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/site:accessallgroups | Prevent | editingteacher | Course | C1 |
And the following "activities" exist:
| activity | name | course | idnumber | groupmode |
| quiz | Test quiz | C1 | quiz1 | 1 |
When I am on the "Test quiz" "mod_quiz > User overrides" page logged in as "teacher"
Then I should see "No groups you can access."
And the "Add user override" "button" should be disabled
Scenario: A non-editing teacher can see the overrides, but not change them
Given the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And the following "mod_quiz > user overrides" exist:
| quiz | user | attempts |
| Test quiz | student1 | 2 |
| Test quiz | student2 | 2 |
And I am on the "Test quiz" "mod_quiz > View" page logged in as "helper"
When I navigate to "Overrides" in current page administration
Then "Student One" "table_row" should exist
And "Student Two" "table_row" should exist
And "Add user override" "button" should not exist
And I should not see "Action"
And "Edit" "link" should not exist in the "Student One" "table_row"
And "Copy" "link" should not exist in the "Student One" "table_row"
And "Delete" "link" should not exist in the "Student One" "table_row"
And I am on the "Test quiz" "mod_quiz > View" page
And I should see "Settings overrides exist (Users: 2)"
@javascript
Scenario: Teachers can see user additional user identity information
Given the following config values are set as admin:
| showuseridentity | email,profile_field_frog |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And the following "mod_quiz > user overrides" exist:
| quiz | user | attempts |
| Test quiz | student1 | 2 |
| Test quiz | student2 | 2 |
When I am on the "Test quiz" "mod_quiz > User overrides" page logged in as "teacher"
Then I should see "yellow frog" in the "Student One" "table_row"
And I should see "prince frog" in the "Student Two" "table_row"
And I press "Add user override"
And I expand the "Override user" autocomplete
And I should see "Kermit"
And I should not see "Student one"
And I should not see "Student two"
And I press "Cancel"
And I click on "Edit" "link" in the "Student One" "table_row"
And I should see "Student One (student1@example.com, yellow frog)"
And I press "Cancel"
And I click on "Delete" "link" in the "Student One" "table_row"
And I should see "Student One (student1@example.com, yellow frog)"
Scenario: Add button disabled if no users
Given the following "courses" exist:
| fullname | shortname | category |
| Course 2 | C2 | 0 |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Other quiz | C2 | quiz2 |
When I am on the "Other quiz" "mod_quiz > User overrides" page logged in as "admin"
Then the "Add user override" "button" should be disabled
@javascript
Scenario: Should see only enrolled users in user selector
Given the following "users" exist:
| username | firstname | lastname | email |
| manager | Max | Manager | man@example.com |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| manager | manager | System | |
And the following "activities" exist:
| activity | name | course | idnumber | groupmode |
| quiz | Test quiz | C1 | quiz1 | 1 |
And the following "role capability" exists:
| role | manager |
| mod/quiz:attempt | allow |
When I am on the "Test quiz" "mod_quiz > User overrides" page logged in as "teacher"
And I press "Add user override"
And I click on "Override user" "field"
And I type "Max Manager"
Then I should see "No suggestions"
@@ -0,0 +1,73 @@
@mod @mod_quiz
Feature: Moving a question to another category should not affect random questions in a quiz
In order for a quiz with random questions to work as expected
Teachers should be able to
Move a question to a different category without affecting the category the random questions in the quiz reference to
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | weeks |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 for testing the Add random question form | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | questioncategory | name |
| Course | C1 | Top | top |
| Course | C1 | top | Default for C1 |
| Course | C1 | Default for C1 | Subcategory |
| Course | C1 | top | Used category |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Used category | essay | Test question to be moved | Write about whatever you want |
@javascript
Scenario: Moving a question should not change the random question
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I apply question bank filter "Category" with value "Used category"
And I press "Add random question"
And I should see "Random (Used category) based on filter condition" on quiz page "1"
And I click on "Configure question" "link" in the "Random (Used category) based on filter condition" "list_item"
And I should see "Used category"
And I am on "Course 1" course homepage
And I navigate to "Question bank" in current page administration
And I apply question bank filter "Category" with value "Used category"
And I click on "Test question to be moved" "checkbox" in the "Test question to be moved" "table_row"
And I click on "With selected" "button"
And I click on question bulk action "move"
And I set the field "Question category" to "Subcategory"
And I press "Move to"
Then I should see "Test question to be moved"
And I should see "Subcategory (1)"
And I am on the "Quiz 1" "mod_quiz > Edit" page
And I should see "Random (Used category) based on filter condition" on quiz page "1"
And I click on "Configure question" "link" in the "Random (Used category) based on filter condition" "list_item"
And I should see "Used category"
@javascript
Scenario: Renaming a random question category should update the random question
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I apply question bank filter "Category" with value "Used category"
And I press "Add random question"
And I should see "Random (Used category) based on filter condition" on quiz page "1"
And I am on the "Course 1" "core_question > course question categories" page
And I click on "Edit this category" "link" in the "Used category" "list_item"
And I set the following fields to these values:
| Name | Used category new |
| Category info | I was edited |
And I press "Save changes"
Then I should see "Used category new"
And I should see "I was edited" in the "Used category new" "list_item"
And I am on the "Quiz 1" "mod_quiz > Edit" page
And I should see "Random (Used category new) based on filter condition" on quiz page "1"
@@ -0,0 +1,57 @@
@mod @mod_quiz
Feature: Several attempts in a quiz
As a student
In order to demonstrate what I know
I need to be able to attempt quizzes and sometimes take multiple attempts
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | One | student1@example.com |
| student2 | Student | One | student2@example.com |
| teacher | Teacher | One | teacher@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 |
| student2 | C1 | student |
| teacher | 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 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | navmethod |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | free |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 2 | 1 |
# Add some attempts
And user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | False |
And user "student2" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | True |
# Add a second attempt by student1
And user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | False |
| 2 | False |
@javascript
Scenario: The redo question buttons are visible after 2 attempts are preset for student1.
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student1"
Then "Re-attempt quiz" "button" should exist
And I should see "Finished" in the "Attempt 1" "list_item"
And I should see "1.00/2.00" in the "Attempt 1" "list_item"
And I should see "Finished" in the "Attempt 2" "list_item"
And I should see "0.00/2.00" in the "Attempt 2" "list_item"
@@ -0,0 +1,148 @@
@mod @mod_quiz
Feature: Settings form fields disabled if not required
In to create quizzes as simply as possible
As a teacher
I don't need to to use certain form fields.
Background:
Given the following "users" exist:
| username | firstname |
| teacher | Teach |
| student1 | Student1 |
| student2 | Student2 |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "activities" exist:
| activity | course | section | name |
| quiz | C1 | 1 | Test quiz 1 |
@javascript
Scenario: Depending on the number of attempts, different form fields are disabled.
When I am on the "Test quiz 1" "quiz activity editing" page logged in as teacher
And I expand all fieldsets
And I set the field "Attempts allowed" to "1"
Then the "Grading method" "field" should be disabled
And the "Each attempt builds on the last" "field" should be disabled
And the "id_delay1_enabled" "field" should be disabled
And the "id_delay2_enabled" "field" should be disabled
And I set the field "Attempts allowed" to "2"
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be disabled
And I set the field "Attempts allowed" to "3"
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be enabled
And I set the field "Attempts allowed" to "Unlimited"
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
# And the "id_delay1_enabled" "field" should be enabled
# And the "id_delay2_enabled" "field" should be enabled
And I press "Save and display"
And I navigate to "Overrides" in current page administration
And I press "Add user override"
And I set the following fields to these values:
| Override user | Student1 |
| Attempts allowed | 3 |
And I press "Save"
And I navigate to "Settings" in current page administration
And I expand all fieldsets
And I set the field "Attempts allowed" to "1"
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be enabled
And I press "Save and display"
And I navigate to "Overrides" in current page administration
And I click on "Edit" "link" in the "region-main" "region"
And I set the field "Attempts allowed" to "2"
And I press "Save"
And I navigate to "Settings" in current page administration
And I expand all fieldsets
And I set the field "Attempts allowed" to "1"
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be disabled
And I press "Save and display"
And I navigate to "Overrides" in current page administration
And I press "Add user override"
And I set the following fields to these values:
| Override user | Student2 |
| Attempts allowed | Unlimited |
And I press "Save"
And I navigate to "Settings" in current page administration
And I expand all fieldsets
And I set the field "Attempts allowed" to "1"
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be enabled
@javascript
Scenario: Depending on whether there is a close date, some review options are disabled.
When I log in as "teacher"
And I add a quiz activity to course "Course 1" section "1"
And I expand all fieldsets
And I set the field "Name" to "Test quiz"
Then the "id_attemptclosed" "checkbox" should be disabled
And the "id_correctnessclosed" "checkbox" should be disabled
And the "id_maxmarksclosed" "checkbox" should be disabled
And the "id_marksclosed" "checkbox" should be disabled
And the "id_specificfeedbackclosed" "checkbox" should be disabled
And the "id_generalfeedbackclosed" "checkbox" should be disabled
And the "id_rightanswerclosed" "checkbox" should be disabled
And the "id_overallfeedbackclosed" "checkbox" should be disabled
And I set the field "id_timeclose_enabled" to "1"
And the "id_attemptclosed" "checkbox" should be enabled
And the "id_correctnessclosed" "checkbox" should be enabled
And the "id_maxmarksclosed" "checkbox" should be enabled
And the "id_marksclosed" "checkbox" should be enabled
And I set the field "id_maxmarksclosed" to "0"
And the "id_marksclosed" "checkbox" should be disabled
And the "id_specificfeedbackclosed" "checkbox" should be enabled
And the "id_generalfeedbackclosed" "checkbox" should be enabled
And the "id_rightanswerclosed" "checkbox" should be enabled
And the "id_overallfeedbackclosed" "checkbox" should be enabled
And I set the field "id_maxmarksduring" to "0"
And the "id_marksduring" "checkbox" should be disabled
And I should not see "Repaginate now"
@javascript
Scenario: If there are quiz attempts, there is not option to repaginate.
Given the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
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 |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | |
When I am on the "Quiz 1" "quiz activity editing" page logged in as teacher
And I expand all fieldsets
Then I should see "Repaginate now"
And user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
And I am on the "Quiz 1" "quiz activity editing" page
And I expand all fieldsets
And I should not see "Repaginate now"
@@ -0,0 +1,519 @@
<?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;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/lib.php');
/**
* Unit tests for the calendar event modification callbacks used
* for dragging and dropping quiz calendar events in the calendar
* UI.
*
* @package mod_quiz
* @category test
* @copyright 2017 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
class calendar_event_modified_test extends \advanced_testcase {
/**
* Create an instance of the quiz activity.
*
* @param array $properties Properties to set on the activity
* @return stdClass Quiz activity instance
*/
protected function create_quiz_instance(array $properties) {
global $DB;
$generator = $this->getDataGenerator();
if (empty($properties['course'])) {
$course = $generator->create_course();
$courseid = $course->id;
} else {
$courseid = $properties['course'];
}
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(array_merge(['course' => $courseid], $properties));
if (isset($properties['timemodified'])) {
// The generator overrides the timemodified value to set it as
// the current time even if a value is provided so we need to
// make sure it's set back to the requested value.
$quiz->timemodified = $properties['timemodified'];
$DB->update_record('quiz', $quiz);
}
return $quiz;
}
/**
* Create a calendar event for a quiz activity instance.
*
* @param \stdClass $quiz The activity instance
* @param array $eventproperties Properties to set on the calendar event
* @return calendar_event
*/
protected function create_quiz_calendar_event(\stdClass $quiz, array $eventproperties) {
$defaultproperties = [
'name' => 'Test event',
'description' => '',
'format' => 1,
'courseid' => $quiz->course,
'groupid' => 0,
'userid' => 2,
'modulename' => 'quiz',
'instance' => $quiz->id,
'eventtype' => QUIZ_EVENT_TYPE_OPEN,
'timestart' => time(),
'timeduration' => 86400,
'visible' => 1
];
return new \calendar_event(array_merge($defaultproperties, $eventproperties));
}
/**
* An unkown event type should not change the quiz instance.
*/
public function test_mod_quiz_core_calendar_event_timestart_updated_unknown_event(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$quiz = $this->create_quiz_instance(['timeopen' => $timeopen, 'timeclose' => $timeclose]);
$event = $this->create_quiz_calendar_event($quiz, [
'eventtype' => QUIZ_EVENT_TYPE_OPEN . "SOMETHING ELSE",
'timestart' => 1
]);
mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
$quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
$this->assertEquals($timeopen, $quiz->timeopen);
$this->assertEquals($timeclose, $quiz->timeclose);
}
/**
* A QUIZ_EVENT_TYPE_OPEN event should update the timeopen property of
* the quiz activity.
*/
public function test_mod_quiz_core_calendar_event_timestart_updated_open_event(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$timemodified = 1;
$newtimeopen = $timeopen - DAYSECS;
$quiz = $this->create_quiz_instance([
'timeopen' => $timeopen,
'timeclose' => $timeclose,
'timemodified' => $timemodified
]);
$event = $this->create_quiz_calendar_event($quiz, [
'eventtype' => QUIZ_EVENT_TYPE_OPEN,
'timestart' => $newtimeopen
]);
mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
$quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
// Ensure the timeopen property matches the event timestart.
$this->assertEquals($newtimeopen, $quiz->timeopen);
// Ensure the timeclose isn't changed.
$this->assertEquals($timeclose, $quiz->timeclose);
// Ensure the timemodified property has been changed.
$this->assertNotEquals($timemodified, $quiz->timemodified);
}
/**
* A QUIZ_EVENT_TYPE_CLOSE event should update the timeclose property of
* the quiz activity.
*/
public function test_mod_quiz_core_calendar_event_timestart_updated_close_event(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$timemodified = 1;
$newtimeclose = $timeclose + DAYSECS;
$quiz = $this->create_quiz_instance([
'timeopen' => $timeopen,
'timeclose' => $timeclose,
'timemodified' => $timemodified
]);
$event = $this->create_quiz_calendar_event($quiz, [
'eventtype' => QUIZ_EVENT_TYPE_CLOSE,
'timestart' => $newtimeclose
]);
mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
$quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
// Ensure the timeclose property matches the event timestart.
$this->assertEquals($newtimeclose, $quiz->timeclose);
// Ensure the timeopen isn't changed.
$this->assertEquals($timeopen, $quiz->timeopen);
// Ensure the timemodified property has been changed.
$this->assertNotEquals($timemodified, $quiz->timemodified);
}
/**
* A QUIZ_EVENT_TYPE_OPEN event should not update the timeopen property of
* the quiz activity if it's an override.
*/
public function test_mod_quiz_core_calendar_event_timestart_updated_open_event_override(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$user = $this->getDataGenerator()->create_user();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$timemodified = 1;
$newtimeopen = $timeopen - DAYSECS;
$quiz = $this->create_quiz_instance([
'timeopen' => $timeopen,
'timeclose' => $timeclose,
'timemodified' => $timemodified
]);
$event = $this->create_quiz_calendar_event($quiz, [
'userid' => $user->id,
'eventtype' => QUIZ_EVENT_TYPE_OPEN,
'timestart' => $newtimeopen
]);
$record = (object) [
'quiz' => $quiz->id,
'userid' => $user->id
];
$DB->insert_record('quiz_overrides', $record);
mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
$quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
// Ensure the timeopen property doesn't change.
$this->assertEquals($timeopen, $quiz->timeopen);
// Ensure the timeclose isn't changed.
$this->assertEquals($timeclose, $quiz->timeclose);
// Ensure the timemodified property has not been changed.
$this->assertEquals($timemodified, $quiz->timemodified);
}
/**
* If a student somehow finds a way to update the quiz calendar event
* then the callback should not update the quiz activity otherwise that
* would be a security issue.
*/
public function test_student_role_cant_update_quiz_activity(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$context = \context_course::instance($course->id);
$roleid = $generator->create_role();
$now = time();
$timeopen = (new \DateTime())->setTimestamp($now);
$newtimeopen = (new \DateTime())->setTimestamp($now)->modify('+1 day');
$quiz = $this->create_quiz_instance([
'course' => $course->id,
'timeopen' => $timeopen->getTimestamp()
]);
$generator->enrol_user($user->id, $course->id, 'student');
$generator->role_assign($roleid, $user->id, $context->id);
$event = $this->create_quiz_calendar_event($quiz, [
'eventtype' => QUIZ_EVENT_TYPE_OPEN,
'timestart' => $timeopen->getTimestamp()
]);
assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true);
$this->setUser($user);
mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
$newquiz = $DB->get_record('quiz', ['id' => $quiz->id]);
// The time open shouldn't have changed even though we updated the calendar
// event.
$this->assertEquals($timeopen->getTimestamp(), $newquiz->timeopen);
}
/**
* A teacher with the capability to modify a quiz module should be
* able to update the quiz activity dates by changing the calendar
* event.
*/
public function test_teacher_role_can_update_quiz_activity(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$context = \context_course::instance($course->id);
$roleid = $generator->create_role();
$now = time();
$timeopen = (new \DateTime())->setTimestamp($now);
$newtimeopen = (new \DateTime())->setTimestamp($now)->modify('+1 day');
$quiz = $this->create_quiz_instance([
'course' => $course->id,
'timeopen' => $timeopen->getTimestamp()
]);
$generator->enrol_user($user->id, $course->id, 'teacher');
$generator->role_assign($roleid, $user->id, $context->id);
$event = $this->create_quiz_calendar_event($quiz, [
'eventtype' => QUIZ_EVENT_TYPE_OPEN,
'timestart' => $newtimeopen->getTimestamp()
]);
assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
$this->setUser($user);
// Trigger and capture the event.
$sink = $this->redirectEvents();
mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
$triggeredevents = $sink->get_events();
$moduleupdatedevents = array_filter($triggeredevents, function($e) {
return is_a($e, 'core\event\course_module_updated');
});
$newquiz = $DB->get_record('quiz', ['id' => $quiz->id]);
// The should be updated along with the event because the user has sufficient
// capabilities.
$this->assertEquals($newtimeopen->getTimestamp(), $newquiz->timeopen);
// Confirm that a module updated event is fired when the module
// is changed.
$this->assertNotEmpty($moduleupdatedevents);
}
/**
* An unkown event type should not have any limits
*/
public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_unknown_event(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$quiz = $this->create_quiz_instance([
'timeopen' => $timeopen,
'timeclose' => $timeclose
]);
$event = $this->create_quiz_calendar_event($quiz, [
'eventtype' => QUIZ_EVENT_TYPE_OPEN . "SOMETHING ELSE",
'timestart' => 1
]);
list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
$this->assertNull($min);
$this->assertNull($max);
}
/**
* The open event should be limited by the quiz's timeclose property, if it's set.
*/
public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_open_event(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$quiz = $this->create_quiz_instance([
'timeopen' => $timeopen,
'timeclose' => $timeclose
]);
$event = $this->create_quiz_calendar_event($quiz, [
'eventtype' => QUIZ_EVENT_TYPE_OPEN,
'timestart' => 1
]);
// The max limit should be bounded by the timeclose value.
list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
$this->assertNull($min);
$this->assertEquals($timeclose, $max[0]);
// No timeclose value should result in no upper limit.
$quiz->timeclose = 0;
list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
$this->assertNull($min);
$this->assertNull($max);
}
/**
* An override event should not have any limits.
*/
public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_override_event(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$quiz = $this->create_quiz_instance([
'course' => $course->id,
'timeopen' => $timeopen,
'timeclose' => $timeclose
]);
$event = $this->create_quiz_calendar_event($quiz, [
'userid' => $user->id,
'eventtype' => QUIZ_EVENT_TYPE_OPEN,
'timestart' => 1
]);
$record = (object) [
'quiz' => $quiz->id,
'userid' => $user->id
];
$DB->insert_record('quiz_overrides', $record);
list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
$this->assertFalse($min);
$this->assertFalse($max);
}
/**
* The close event should be limited by the quiz's timeopen property, if it's set.
*/
public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_close_event(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$quiz = $this->create_quiz_instance([
'timeopen' => $timeopen,
'timeclose' => $timeclose
]);
$event = $this->create_quiz_calendar_event($quiz, [
'eventtype' => QUIZ_EVENT_TYPE_CLOSE,
'timestart' => 1,
]);
// The max limit should be bounded by the timeclose value.
list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
$this->assertEquals($timeopen, $min[0]);
$this->assertNull($max);
// No timeclose value should result in no upper limit.
$quiz->timeopen = 0;
list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
$this->assertNull($min);
$this->assertNull($max);
}
/**
* When the close date event is changed and it results in the time close value of
* the quiz being updated then the open quiz attempts should also be updated.
*/
public function test_core_calendar_event_timestart_updated_update_quiz_attempt(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$teacher = $generator->create_user();
$student = $generator->create_user();
$course = $generator->create_course();
$context = \context_course::instance($course->id);
$roleid = $generator->create_role();
$now = time();
$timelimit = 600;
$timeopen = (new \DateTime())->setTimestamp($now);
$timeclose = (new \DateTime())->setTimestamp($now)->modify('+1 day');
// The new close time being earlier than the time open + time limit should
// result in an update to the quiz attempts.
$newtimeclose = $timeopen->getTimestamp() + $timelimit - 10;
$quiz = $this->create_quiz_instance([
'course' => $course->id,
'timeopen' => $timeopen->getTimestamp(),
'timeclose' => $timeclose->getTimestamp(),
'timelimit' => $timelimit
]);
$generator->enrol_user($student->id, $course->id, 'student');
$generator->enrol_user($teacher->id, $course->id, 'teacher');
$generator->role_assign($roleid, $teacher->id, $context->id);
$event = $this->create_quiz_calendar_event($quiz, [
'eventtype' => QUIZ_EVENT_TYPE_CLOSE,
'timestart' => $newtimeclose
]);
assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
$attemptid = $DB->insert_record(
'quiz_attempts',
[
'quiz' => $quiz->id,
'userid' => $student->id,
'state' => 'inprogress',
'timestart' => $timeopen->getTimestamp(),
'timecheckstate' => 0,
'layout' => '',
'uniqueid' => 1
]
);
$this->setUser($teacher);
mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
$quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
$attempt = $DB->get_record('quiz_attempts', ['id' => $attemptid]);
// When the close date is changed so that it's earlier than the time open
// plus the time limit of the quiz then the attempt's timecheckstate should
// be updated to the new time close date of the quiz.
$this->assertEquals($newtimeclose, $attempt->timecheckstate);
$this->assertEquals($newtimeclose, $quiz->timeclose);
}
}
+506
View File
@@ -0,0 +1,506 @@
<?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/>.
declare(strict_types=1);
namespace mod_quiz;
use advanced_testcase;
use cm_info;
use core_completion\cm_completion_details;
use grade_item;
use mod_quiz\completion\custom_completion;
use question_engine;
use mod_quiz\quiz_settings;
use stdClass;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/completionlib.php');
/**
* Class for unit testing mod_quiz/custom_completion.
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \mod_quiz\completion\custom_completion
*/
class custom_completion_test extends advanced_testcase {
/**
* Setup function for all tests.
*
* @param array $completionoptions ['nbstudents'] => int, ['qtype'] => string, ['quizoptions'] => array
* @return array [$students, $quiz, $cm, $litecm]
*/
private function setup_quiz_for_testing_completion(array $completionoptions): array {
global $CFG, $DB;
$this->resetAfterTest(true);
// Enable completion before creating modules, otherwise the completion data is not written in DB.
$CFG->enablecompletion = true;
// Create a course and students.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$course = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
$students = [];
$sumgrades = $completionoptions['sumgrades'] ?? 1;
$nbquestions = $completionoptions['nbquestions'] ?? 1;
for ($i = 0; $i < $completionoptions['nbstudents']; $i++) {
$students[$i] = $this->getDataGenerator()->create_user();
$this->assertTrue($this->getDataGenerator()->enrol_user($students[$i]->id, $course->id, $studentrole->id));
}
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$data = array_merge([
'course' => $course->id,
'grade' => 100.0,
'questionsperpage' => 0,
'sumgrades' => $sumgrades,
'completion' => COMPLETION_TRACKING_AUTOMATIC
], $completionoptions['quizoptions']);
$quiz = $quizgenerator->create_instance($data);
$litecm = get_coursemodule_from_id('quiz', $quiz->cmid);
$cm = cm_info::create($litecm);
// Create a question.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
for ($i = 0; $i < $nbquestions; $i++) {
$overrideparams = ['category' => $cat->id];
if (isset($completionoptions['questiondefaultmarks'][$i])) {
$overrideparams['defaultmark'] = $completionoptions['questiondefaultmarks'][$i];
}
$question = $questiongenerator->create_question($completionoptions['qtype'], null, $overrideparams);
quiz_add_quiz_question($question->id, $quiz);
}
// Set grade to pass.
$item = grade_item::fetch(['courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'quiz',
'iteminstance' => $quiz->id, 'outcomeid' => null]);
$item->gradepass = 80;
$item->update();
return [
$students,
$quiz,
$cm,
$litecm
];
}
/**
* Helper function for tests.
* Starts an attempt, processes responses and finishes the attempt.
*
* @param array $attemptoptions ['quiz'] => object, ['student'] => object, ['tosubmit'] => array, ['attemptnumber'] => int
*/
private function do_attempt_quiz(array $attemptoptions) {
$quizobj = quiz_settings::create((int) $attemptoptions['quiz']->id);
// Start the passing attempt.
$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, $attemptoptions['attemptnumber'], false, $timenow, false,
$attemptoptions['student']->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptoptions['attemptnumber'], $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Process responses from the student.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_submitted_actions($timenow, false, $attemptoptions['tosubmit']);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$attemptobj->process_finish($timenow, false);
}
/**
* Test checking the completion state of a quiz base on core's completionpassgrade criteria.
* The quiz requires a passing grade to be completed.
*/
public function test_completionpass(): void {
list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
'nbstudents' => 2,
'qtype' => 'numerical',
'quizoptions' => [
'completionusegrade' => 1,
'completionpassgrade' => 1
]
]);
list($passstudent, $failstudent) = $students;
// Do a passing attempt.
$this->do_attempt_quiz([
'quiz' => $quiz,
'student' => $passstudent,
'attemptnumber' => 1,
'tosubmit' => [1 => ['answer' => '3.14']]
]);
$completioninfo = new \completion_info($cm->get_course());
$completiondetails = new cm_completion_details($completioninfo, $cm, (int) $passstudent->id);
// Check the results.
$this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
// Do a failing attempt.
$this->do_attempt_quiz([
'quiz' => $quiz,
'student' => $failstudent,
'attemptnumber' => 1,
'tosubmit' => [1 => ['answer' => '0']]
]);
$completiondetails = new cm_completion_details($completioninfo, $cm, (int) $failstudent->id);
// Check the results.
$this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
}
/**
* Test checking the completion state of a quiz.
* To be completed, this quiz requires either a passing grade or for all attempts to be used up.
*
* @covers ::get_state
* @covers ::get_custom_rule_descriptions
*/
public function test_completionexhausted(): void {
list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
'nbstudents' => 2,
'qtype' => 'numerical',
'quizoptions' => [
'attempts' => 2,
'completionusegrade' => 1,
'completionpassgrade' => 1,
'completionattemptsexhausted' => 1
]
]);
list($passstudent, $exhauststudent) = $students;
// Start a passing attempt.
$this->do_attempt_quiz([
'quiz' => $quiz,
'student' => $passstudent,
'attemptnumber' => 1,
'tosubmit' => [1 => ['answer' => '3.14']]
]);
$completioninfo = new \completion_info($cm->get_course());
// Check the results. Quiz is completed by $passstudent because of passing grade.
$studentid = (int) $passstudent->id;
$customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
$this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
$this->assertEquals(
'Receive a pass grade or complete all available attempts',
$customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
);
// Do a failing attempt.
$this->do_attempt_quiz([
'quiz' => $quiz,
'student' => $exhauststudent,
'attemptnumber' => 1,
'tosubmit' => [1 => ['answer' => '0']]
]);
// Check the results. Quiz is not completed by $exhauststudent yet because of failing grade and of remaining attempts.
$studentid = (int) $exhauststudent->id;
$customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
$this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
$this->assertEquals(
'Receive a pass grade or complete all available attempts',
$customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
);
// Do a second failing attempt.
$this->do_attempt_quiz([
'quiz' => $quiz,
'student' => $exhauststudent,
'attemptnumber' => 2,
'tosubmit' => [1 => ['answer' => '0']]
]);
// Check the results. Quiz is completed by $exhauststudent because there are no remaining attempts.
$customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
$this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
$this->assertEquals(
'Receive a pass grade or complete all available attempts',
$customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
);
}
/**
* Test checking the completion state of a quiz.
* To be completed, this quiz requires a minimum number of attempts.
*
* @covers ::get_state
* @covers ::get_custom_rule_descriptions
*/
public function test_completionminattempts(): void {
list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
'nbstudents' => 1,
'qtype' => 'essay',
'quizoptions' => [
'completionminattemptsenabled' => 1,
'completionminattempts' => 2
]
]);
list($student) = $students;
// Do a first attempt.
$this->do_attempt_quiz([
'quiz' => $quiz,
'student' => $student,
'attemptnumber' => 1,
'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
]);
// Check the results. Quiz is not completed yet because only one attempt was done.
$customcompletion = new custom_completion($cm, (int) $student->id);
$this->assertArrayHasKey('completionminattempts', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionminattempts'));
$this->assertEquals(
'Make attempts: 2',
$customcompletion->get_custom_rule_descriptions()['completionminattempts']
);
// Do a second attempt.
$this->do_attempt_quiz([
'quiz' => $quiz,
'student' => $student,
'attemptnumber' => 2,
'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
]);
// Check the results. Quiz is completed by $student because two attempts were done.
$customcompletion = new custom_completion($cm, (int) $student->id);
$this->assertArrayHasKey('completionminattempts', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionminattempts'));
$this->assertEquals(
'Make attempts: 2',
$customcompletion->get_custom_rule_descriptions()['completionminattempts']
);
}
/**
* Test for get_defined_custom_rules().
*
* @covers ::get_defined_custom_rules
*/
public function test_get_defined_custom_rules(): void {
$rules = custom_completion::get_defined_custom_rules();
$this->assertCount(2, $rules);
$this->assertEquals(
['completionpassorattemptsexhausted', 'completionminattempts'],
$rules
);
}
/**
* Test update moduleinfo.
*
* @covers \update_moduleinfo
*/
public function test_update_moduleinfo(): void {
$this->setAdminUser();
// We need lite cm object not a full cm because update_moduleinfo is not allow some properties to be updated.
list($students, $quiz, $cm, $litecm) = $this->setup_quiz_for_testing_completion([
'nbstudents' => 1,
'qtype' => 'numerical',
'nbquestions' => 2,
'sumgrades' => 100,
'questiondefaultmarks' => [20, 80],
'quizoptions' => [
'completionusegrade' => 1,
'completionpassgrade' => 1,
'completionview' => 0,
]
]);
$course = $cm->get_course();
list($student) = $students;
// Do a first attempt with a pass marks = 20.
$this->do_attempt_quiz([
'quiz' => $quiz,
'student' => $student,
'attemptnumber' => 1,
'tosubmit' => [1 => ['answer' => '3.14']]
]);
$completioninfo = new \completion_info($course);
$cminfo = \cm_info::create($cm);
$completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
// Check the results. Completion is fail because gradepass = 80.
$this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
// Update quiz with passgrade = 20 and use highest grade to calculate.
$moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 20, QUIZ_GRADEHIGHEST);
update_moduleinfo($litecm, $moduleinfo, $course, null);
$completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
// Check the results. Completion is pass.
$this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
// Do a second attempt with pass marks = 80.
$this->do_attempt_quiz([
'quiz' => $quiz,
'student' => $student,
'attemptnumber' => 2,
'tosubmit' => [2 => ['answer' => '3.14']]
]);
// Update quiz with gradepass = 80 and use highest grade to calculate completion.
$moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 80, QUIZ_GRADEHIGHEST);
update_moduleinfo($litecm, $moduleinfo, $course, null);
$completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
// Check the results. Completion is pass.
$this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
// Update quiz with gradepass = 80 and use average grade to calculate completion.
$moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 80, QUIZ_GRADEAVERAGE);
update_moduleinfo($litecm, $moduleinfo, $course, null);
$completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
// Check the results. Completion is fail because student grade = 50.
$this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
// Update quiz with gradepass = 50 and use average grade to calculate completion.
$moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_GRADEAVERAGE);
update_moduleinfo($litecm, $moduleinfo, $course, null);
$completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
// Check the results. Completion is pass.
$this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
// Update quiz with gradepass = 50 and use first attempt grade to calculate completion.
$moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_ATTEMPTFIRST);
update_moduleinfo($litecm, $moduleinfo, $course, null);
$completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
// Check the results. Completion is fail.
$this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
// Update quiz with gradepass = 50 and use last attempt grade to calculate completion.
$moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_ATTEMPTLAST);
update_moduleinfo($litecm, $moduleinfo, $course, null);
$completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
// Check the results. Completion is fail.
$this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
}
/**
* Set up moduleinfo object sample data for quiz instance.
*
* @param cm_info $cm course-module instance
* @param stdClass $quiz quiz instance data.
* @param stdClass $course Course related data.
* @param int $gradepass Grade to pass and completed completion.
* @param string $grademethod grade attempt method.
* @return stdClass
*/
private function prepare_module_info(cm_info $cm, stdClass $quiz, stdClass $course,
int $gradepass, string $grademethod): \stdClass {
$grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
// Module test values.
$moduleinfo = new \stdClass();
$moduleinfo->coursemodule = $cm->id;
$moduleinfo->section = 1;
$moduleinfo->course = $course->id;
$moduleinfo->groupingid = $grouping->id;
$draftideditor = 0;
file_prepare_draft_area($draftideditor, null, null, null, null);
$moduleinfo->introeditor = ['text' => 'This is a module', 'format' => FORMAT_HTML, 'itemid' => $draftideditor];
$moduleinfo->modulename = 'quiz';
$moduleinfo->quizpassword = '';
$moduleinfo->cmidnumber = '';
$moduleinfo->maxmarksopen = 1;
$moduleinfo->marksopen = 1;
$moduleinfo->visible = 1;
$moduleinfo->visibleoncoursepage = 1;
$moduleinfo->completion = COMPLETION_TRACKING_AUTOMATIC;
$moduleinfo->completionview = COMPLETION_VIEW_NOT_REQUIRED;
$moduleinfo->name = $quiz->name;
$moduleinfo->timeopen = $quiz->timeopen;
$moduleinfo->timeclose = $quiz->timeclose;
$moduleinfo->timelimit = $quiz->timelimit;
$moduleinfo->graceperiod = $quiz->graceperiod;
$moduleinfo->decimalpoints = $quiz->decimalpoints;
$moduleinfo->questiondecimalpoints = $quiz->questiondecimalpoints;
$moduleinfo->gradepass = $gradepass;
$moduleinfo->grademethod = $grademethod;
return $moduleinfo;
}
}
+175
View File
@@ -0,0 +1,175 @@
<?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/>.
/**
* Contains unit tests for mod_quiz\dates.
*
* @package mod_quiz
* @category test
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_quiz;
use advanced_testcase;
use cm_info;
use core\activity_dates;
/**
* Class for unit testing mod_quiz\dates.
*
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates_test extends advanced_testcase {
/**
* Data provider for get_dates_for_module().
* @return array[]
*/
public function get_dates_for_module_provider(): array {
$now = time();
$before = $now - DAYSECS;
$earlier = $before - DAYSECS;
$after = $now + DAYSECS;
$later = $after + DAYSECS;
return [
'without any dates' => [
null, null, null, null, null, null, []
],
'only with opening time' => [
$after, null, null, null, null, null, [
['label' => get_string('activitydate:opens', 'course'), 'timestamp' => $after, 'dataid' => 'timeopen'],
]
],
'only with closing time' => [
null, $after, null, null, null, null, [
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $after, 'dataid' => 'timeclose'],
]
],
'with both times' => [
$after, $later, null, null, null, null, [
['label' => get_string('activitydate:opens', 'course'), 'timestamp' => $after, 'dataid' => 'timeopen'],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later, 'dataid' => 'timeclose'],
]
],
'between the dates' => [
$before, $after, null, null, null, null, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $before, 'dataid' => 'timeopen'],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $after, 'dataid' => 'timeclose'],
]
],
'dates are past' => [
$earlier, $before, null, null, null, null, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier, 'dataid' => 'timeopen'],
['label' => get_string('activitydate:closed', 'course'), 'timestamp' => $before, 'dataid' => 'timeclose'],
]
],
'with user override' => [
$before, $after, $earlier, $later, null, null, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier, 'dataid' => 'timeopen'],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later, 'dataid' => 'timeclose'],
]
],
'with group override' => [
$before, $after, null, null, $earlier, $later, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier, 'dataid' => 'timeopen'],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later, 'dataid' => 'timeclose'],
]
],
'with both user and group overrides' => [
$before, $after, $earlier, $later, $earlier - DAYSECS, $later + DAYSECS, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier, 'dataid' => 'timeopen'],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later, 'dataid' => 'timeclose'],
]
],
];
}
/**
* Test for get_dates_for_module().
*
* @dataProvider get_dates_for_module_provider
* @param int|null $timeopen Time of opening the quiz.
* @param int|null $timeclose Time of closing the quiz.
* @param int|null $usertimeopen The user override for opening the quiz.
* @param int|null $usertimeclose The user override for closing the quiz.
* @param int|null $grouptimeopen The group override for opening the quiz.
* @param int|null $grouptimeclose The group override for closing the quiz.
* @param array $expected The expected value of calling get_dates_for_module()
*/
public function test_get_dates_for_module(?int $timeopen, ?int $timeclose,
?int $usertimeopen, ?int $usertimeclose,
?int $grouptimeopen, ?int $grouptimeclose,
array $expected): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$course = $generator->create_course();
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id);
$data = ['course' => $course->id];
if ($timeopen) {
$data['timeopen'] = $timeopen;
}
if ($timeclose) {
$data['timeclose'] = $timeclose;
}
$quiz = $quizgenerator->create_instance($data);
if ($usertimeopen || $usertimeclose || $grouptimeopen || $grouptimeclose) {
$generator->enrol_user($user->id, $course->id);
$group = $generator->create_group(['courseid' => $course->id]);
$generator->create_group_member(['groupid' => $group->id, 'userid' => $user->id]);
if ($usertimeopen || $usertimeclose) {
$quizgenerator->create_override([
'quiz' => $quiz->id,
'userid' => $user->id,
'timeopen' => $usertimeopen,
'timeclose' => $usertimeclose,
]);
}
if ($grouptimeopen || $grouptimeclose) {
$quizgenerator->create_override([
'quiz' => $quiz->id,
'groupid' => $group->id,
'timeopen' => $grouptimeopen,
'timeclose' => $grouptimeclose,
]);
}
}
$this->setUser($user);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
// Make sure we're using a cm_info object.
$cm = cm_info::create($cm);
$dates = activity_dates::get_dates_for_module($cm, (int) $user->id);
$this->assertEquals($expected, $dates);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+321
View File
@@ -0,0 +1,321 @@
<?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;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../../webservice/tests/helpers.php');
use coding_exception;
use core_question_generator;
use externallib_advanced_testcase;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use required_capability_exception;
use stdClass;
/**
* Test for the grade_items CRUD service.
*
* @package mod_quiz
* @category external
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\external\create_grade_items
* @covers \mod_quiz\external\delete_grade_items
* @covers \mod_quiz\external\update_grade_items
* @covers \mod_quiz\structure
*/
final class grade_items_test extends externallib_advanced_testcase {
public function test_create_grade_items_service_works(): void {
$quizobj = $this->create_quiz_with_two_grade_items();
create_grade_items::execute($quizobj->get_quizid(), [
['name' => 'Speaking'],
['name' => 'Writing'],
]);
$structure = $quizobj->get_structure();
$items = array_values($structure->get_grade_items());
$this->assertEquals('Listening', $items[0]->name);
$this->assertEquals('Reading', $items[1]->name);
$this->assertEquals('Speaking', $items[2]->name);
$this->assertEquals('Writing', $items[3]->name);
}
public function test_create_grade_items_service_checks_permissions(): void {
$quizobj = $this->create_quiz_with_two_grade_items();
$unprivilegeduser = $this->getDataGenerator()->create_user();
$this->setUser($unprivilegeduser);
$this->expectException(required_capability_exception::class);
create_grade_items::execute($quizobj->get_quizid(), []);
}
public function test_update_grade_items_service_works(): void {
$quizobj = $this->create_quiz_with_two_grade_items();
$structure = $quizobj->get_structure();
$items = array_values($structure->get_grade_items());
update_grade_items::execute($quizobj->get_quizid(), [
['id' => $items[0]->id, 'name' => 'Speaking'],
['id' => $items[1]->id, 'name' => null],
]);
$structure = $quizobj->get_structure();
$updateditems = $structure->get_grade_items();
$this->assertEquals('Speaking', $updateditems[$items[0]->id]->name);
$this->assertEquals($items[1]->name, $updateditems[$items[1]->id]->name);
}
public function test_update_grade_items_service_checks_permissions(): void {
$quizobj = $this->create_quiz_with_two_grade_items();
$unprivilegeduser = $this->getDataGenerator()->create_user();
$this->setUser($unprivilegeduser);
$this->expectException(required_capability_exception::class);
update_grade_items::execute($quizobj->get_quizid(), []);
}
public function test_delete_grade_items_service_works(): void {
$quizobj = $this->create_quiz_with_two_grade_items();
$structure = $quizobj->get_structure();
$items = array_values($structure->get_grade_items());
$structure->update_slot_grade_item($structure->get_slot_by_number(1), null);
$structure->update_slot_grade_item($structure->get_slot_by_number(2), null);
delete_grade_items::execute($quizobj->get_quizid(), [['id' => $items[0]->id]]);
$structure = $quizobj->get_structure();
$updateditems = $structure->get_grade_items();
$this->assertCount(1, $updateditems);
$this->assertEquals('Reading', $updateditems[$items[1]->id]->name);
}
public function test_cant_delete_grade_item_that_is_used(): void {
$quizobj = $this->create_quiz_with_two_grade_items();
$structure = $quizobj->get_structure();
$items = array_values($structure->get_grade_items());
$this->expectException(coding_exception::class);
delete_grade_items::execute($quizobj->get_quizid(), [['id' => $items[0]->id]]);
}
public function test_delete_grade_items_service_checks_permissions(): void {
$quizobj = $this->create_quiz_with_two_grade_items();
$unprivilegeduser = $this->getDataGenerator()->create_user();
$this->setUser($unprivilegeduser);
$structure = $quizobj->get_structure();
$items = array_values($structure->get_grade_items());
$this->expectException(required_capability_exception::class);
delete_grade_items::execute($quizobj->get_quizid(), [['id' => $items[0]->id]]);
}
public function test_get_edit_grading_page_data_service_works(): void {
global $PAGE;
$PAGE->set_url('/');
$quizobj = $this->create_quiz_with_two_grade_items();
$jsondata = get_edit_grading_page_data::execute($quizobj->get_quizid());
$this->assertJson($jsondata);
$data = json_decode($jsondata);
$this->assertEquals($quizobj->get_quizid(), $data->quizid);
}
public function test_get_edit_grading_page_data_service_checks_permissions(): void {
$quizobj = $this->create_quiz_with_two_grade_items();
$unprivilegeduser = $this->getDataGenerator()->create_user();
$this->setUser($unprivilegeduser);
$this->expectException(required_capability_exception::class);
get_edit_grading_page_data::execute($quizobj->get_quizid());
}
/**
* Create a quiz of two shortanswer questions, each contributing to a different grade item.
*
* @return quiz_settings the newly created quiz.
*/
protected function create_quiz_with_two_grade_items(): quiz_settings {
global $SITE;
$this->resetAfterTest();
$this->setAdminUser();
// Make a quiz.
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id]);
// Create two question.
/** @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]);
// Add them to the quiz.
quiz_add_quiz_question($saq1->id, $quiz, 0, 1);
quiz_add_quiz_question($saq2->id, $quiz, 0, 1);
// Create two quiz grade items.
$listeninggrade = $quizgenerator->create_grade_item(['quizid' => $quiz->id, 'name' => 'Listening']);
$readinggrade = $quizgenerator->create_grade_item(['quizid' => $quiz->id, 'name' => 'Reading']);
// Set the questions to use those grade items.
$quizobj = quiz_settings::create($quiz->id);
$structure = $quizobj->get_structure();
$structure->update_slot_grade_item($structure->get_slot_by_number(1), $listeninggrade->id);
$structure->update_slot_grade_item($structure->get_slot_by_number(2), $readinggrade->id);
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
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_with_descriptions(): 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();
$desc1 = $questiongenerator->create_question('description', null, ['category' => $cat->id]);
$desc2 = $questiongenerator->create_question('description', null, ['category' => $cat->id]);
$saq1 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
// Add them to the quiz.
quiz_add_quiz_question($desc1->id, $quiz, 1);
quiz_add_quiz_question($desc2->id, $quiz, 2);
quiz_add_quiz_question($saq1->id, $quiz, 2, 7);
// 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, 'Introduction');
$structure->add_section_heading(2, 'The question');
// 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(1, $gradeitems);
$this->assertEquals('The question', $gradeitems[0]->name);
$this->assertEquals(1, $gradeitems[0]->sortorder);
$this->assertNull($structure->get_slot_by_number(1)->quizgradeitemid);
$this->assertNull($structure->get_slot_by_number(2)->quizgradeitemid);
$this->assertEquals($gradeitems[0]->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());
}
}
+223
View File
@@ -0,0 +1,223 @@
<?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;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../../webservice/tests/helpers.php');
/**
* Tests for override webservices
*
* @package mod_quiz
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\external\get_overrides
* @covers \mod_quiz\external\save_overrides
* @covers \mod_quiz\external\delete_overrides
*/
final class override_test extends \externallib_advanced_testcase {
/**
* Creates a quiz for testing.
*
* @return object $quiz
*/
private function create_quiz(): object {
$course = $this->getDataGenerator()->create_course();
return $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
}
/**
* Provides values to test_get_overrides
*
* @return array
*/
public static function get_override_provider(): array {
return [
'quiz that exists' => [
'quizid' => ':quizid',
],
'quiz that does not exist' => [
'quizid' => -1,
'expectedexception' => \dml_missing_record_exception::class,
],
];
}
/**
* Tests get_overrides
*
* @param int|string $quizid
* @param string $expectedexception
* @dataProvider get_override_provider
*/
public function test_get_overrides(int|string $quizid, string $expectedexception = ''): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$quiz = $this->create_quiz();
// Create an override.
$DB->insert_record('quiz_overrides', ['quiz' => $quiz->id]);
// Replace placeholders.
if ($quizid == ":quizid") {
$quizid = $quiz->id;
}
if (!empty($expectedexception)) {
$this->expectException($expectedexception);
}
$result = get_overrides::execute($quizid);
$this->assertNotEmpty($result);
}
/**
* Provides values to test_save_overrides
*
* @return array
*/
public static function save_overrides_provider(): array {
return [
'good insert' => [
'data' => [
'timeopen' => 999,
],
],
'bad insert' => [
'data' => [
'id' => ':existingid',
'timeopen' => -1,
],
'expectedexception' => \invalid_parameter_exception::class,
],
'good update' => [
'data' => [
'timeopen' => 999,
],
],
'bad update' => [
'data' => [
'id' => ':existingid',
'timeopen' => -1,
],
'expectedexception' => \invalid_parameter_exception::class,
],
];
}
/**
* Tests save_overrides
*
* @dataProvider save_overrides_provider
* @param array $data
* @param string $expectedexception
*/
public function test_save_overrides(array $data, string $expectedexception = ''): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$quiz = $this->create_quiz();
$user = $this->getDataGenerator()->create_user();
if (!empty($data['id'])) {
$data['id'] = $DB->insert_record('quiz_overrides', ['quiz' => $quiz->id, 'userid' => $user->id]);
}
// Make a new user to insert a new override for.
$user = $this->getDataGenerator()->create_user();
$data = array_merge($data, ['userid' => $user->id]);
$payload = [
'quizid' => $quiz->id,
'overrides' => [
$data,
],
];
if (!empty($expectedexception)) {
$this->expectException($expectedexception);
}
$result = save_overrides::execute($payload);
// If has reached here, but not thrown exception and was expected to, fail the test.
if ($expectedexception) {
$this->fail("Expected exception " . $expectedexception . " was not thrown");
}
$this->assertNotEmpty($result['ids']);
$this->assertCount(1, $result['ids']);
}
/**
* Provides values to test_delete_overrides
*
* @return array
*/
public static function delete_overrides_provider(): array {
return [
'delete existing override' => [
'id' => ':existingid',
],
'delete override that does not exist' => [
'id' => -1,
'expectedexception' => \invalid_parameter_exception::class,
],
];
}
/**
* Tests delete_overrides
*
* @dataProvider delete_overrides_provider
* @param int|string $id
* @param string $expectedexception
*/
public function test_delete_overrides(int|string $id, string $expectedexception = ''): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$quiz = $this->create_quiz();
$user = $this->getDataGenerator()->create_user();
if ($id == ':existingid') {
$id = $DB->insert_record('quiz_overrides', ['quiz' => $quiz->id, 'userid' => $user->id]);
}
if (!empty($expectedexception)) {
$this->expectException($expectedexception);
}
$result = delete_overrides::execute(['quizid' => $quiz->id, 'ids' => [$id]]);
// If has reached here, but not thrown exception and was expected to, fail the test.
if ($expectedexception) {
$this->fail("Expected exception " . $expectedexception . " was not thrown");
}
$this->assertNotEmpty($result['ids']);
$this->assertContains($id, $result['ids']);
}
}
+181
View File
@@ -0,0 +1,181 @@
<?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;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../../webservice/tests/helpers.php');
use coding_exception;
use core_question_generator;
use externallib_advanced_testcase;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use required_capability_exception;
use stdClass;
/**
* Test for the reopen_attempt and get_reopen_attempt_confirmation services.
*
* @package mod_quiz
* @category external
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\external\reopen_attempt
* @covers \mod_quiz\external\get_reopen_attempt_confirmation
*/
class reopen_attempt_test extends externallib_advanced_testcase {
/** @var stdClass|null if we make a quiz attempt, we store the student object here. */
protected $student;
public function test_reopen_attempt_service_works(): void {
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
reopen_attempt::execute($attemptid);
$attemptobj = quiz_attempt::create($attemptid);
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
}
public function test_reopen_attempt_service_checks_permissions(): void {
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
$unprivilegeduser = $this->getDataGenerator()->create_user();
$this->setUser($unprivilegeduser);
$this->expectException(required_capability_exception::class);
reopen_attempt::execute($attemptid);
}
public function test_reopen_attempt_service_checks_attempt_state(): void {
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question(quiz_attempt::IN_PROGRESS);
$this->expectExceptionMessage("Attempt $attemptid is in the wrong state (In progress) to be reopened.");
reopen_attempt::execute($attemptid);
}
public function test_get_reopen_attempt_confirmation_staying_open(): void {
global $DB;
[$attemptid, $quizid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
$DB->set_field('quiz', 'timeclose', 0, ['id' => $quizid]);
$message = get_reopen_attempt_confirmation::execute($attemptid);
$this->assertEquals('<p>This will reopen attempt 1 by ' . fullname($this->student) .
'.</p><p>The attempt will remain open and can be continued.</p>',
$message);
}
public function test_get_reopen_attempt_confirmation_staying_open_until(): void {
global $DB;
[$attemptid, $quizid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
$timeclose = time() + HOURSECS;
$DB->set_field('quiz', 'timeclose', $timeclose, ['id' => $quizid]);
$message = get_reopen_attempt_confirmation::execute($attemptid);
$this->assertEquals('<p>This will reopen attempt 1 by ' . fullname($this->student) .
'.</p><p>The attempt will remain open and can be continued until the quiz closes on ' .
userdate($timeclose) . '.</p>',
$message);
}
public function test_get_reopen_attempt_confirmation_submitting(): void {
global $DB;
[$attemptid, $quizid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
$timeclose = time() - HOURSECS;
$DB->set_field('quiz', 'timeclose', $timeclose, ['id' => $quizid]);
$message = get_reopen_attempt_confirmation::execute($attemptid);
$this->assertEquals('<p>This will reopen attempt 1 by ' . fullname($this->student) .
'.</p><p>The attempt will be immediately submitted for grading.</p>',
$message);
}
public function test_get_reopen_attempt_confirmation_service_checks_permissions(): void {
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
$unprivilegeduser = $this->getDataGenerator()->create_user();
$this->setUser($unprivilegeduser);
$this->expectException(required_capability_exception::class);
get_reopen_attempt_confirmation::execute($attemptid);
}
public function test_get_reopen_attempt_confirmation_service_checks_attempt_state(): void {
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question(quiz_attempt::IN_PROGRESS);
$this->expectExceptionMessage("Attempt $attemptid is in the wrong state (In progress) to be reopened.");
get_reopen_attempt_confirmation::execute($attemptid);
}
/**
* Create a quiz of one shortanswer question and an attempt in a given state.
*
* @param string $attemptstate the desired attempt state. quiz_attempt::ABANDONED or ::IN_PROGRESS.
* @return array with two elements, the attempt id and the quiz id.
*/
protected function create_attempt_at_quiz_with_one_shortanswer_question(
string $attemptstate = quiz_attempt::ABANDONED
): array {
global $SITE;
$this->resetAfterTest();
// Make a quiz.
$timeclose = time() + HOURSECS;
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance([
'course' => $SITE->id,
'timeclose' => $timeclose,
'overduehandling' => 'autoabandon'
]);
// Create a question.
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
// Add them to the quiz.
$quizobj = quiz_settings::create($quiz->id);
quiz_add_quiz_question($saq->id, $quiz, 0, 1);
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
// Make a user to do the quiz.
$this->student = $this->getDataGenerator()->create_user();
$this->setUser($this->student);
$quizobj = quiz_settings::create($quiz->id, $this->student->id);
// Start the attempt.
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
$attemptobj = quiz_attempt::create($attempt->id);
if ($attemptstate === quiz_attempt::ABANDONED) {
// Attempt goes overdue (e.g. if cron ran).
$attemptobj->process_abandon($timeclose + 2 * get_config('quiz', 'graceperiodmin'), false);
} else if ($attemptstate !== quiz_attempt::IN_PROGRESS) {
throw new coding_exception('Status ' . $attemptstate . ' not currently supported.');
}
// Set current user to admin before we return.
$this->setAdminUser();
return [$attemptobj->get_attemptid(), $attemptobj->get_quizid()];
}
}
+112
View File
@@ -0,0 +1,112 @@
<?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;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../../webservice/tests/helpers.php');
use core_question_generator;
use externallib_advanced_testcase;
use mod_quiz\quiz_settings;
use required_capability_exception;
/**
* Test for the update_slots service.
*
* @package mod_quiz
* @category external
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\external\update_slots
*/
final class update_slots_test extends externallib_advanced_testcase {
public function test_update_slots_service_works(): void {
global $DB;
$quizobj = $this->create_quiz_with_two_shortanswer_questions();
$this->setAdminUser();
$structure = $quizobj->get_structure();
// No changes to slot 1.
$slot1data = [
'id' => $structure->get_slot_by_number(1)->id,
];
// Change everything in slot 2.
$slot2data = [
'id' => $structure->get_slot_by_number(2)->id,
'displaynumber' => '1b',
'requireprevious' => true,
'maxmark' => 7,
'quizgradeitemid' => 123,
];
update_slots::execute($quizobj->get_quizid(), [$slot1data, $slot2data]);
$slot = $DB->get_record('quiz_slots', ['id' => $slot2data['id']]);
$this->assertEquals('1b', $slot->displaynumber);
$this->assertTrue((bool) $slot->requireprevious);
$this->assertEquals(7, $slot->maxmark);
$this->assertEquals(123, $slot->quizgradeitemid);
}
public function test_update_slots_checks_permissions(): void {
$quizobj = $this->create_quiz_with_two_shortanswer_questions();
$unprivilegeduser = $this->getDataGenerator()->create_user();
$this->setUser($unprivilegeduser);
$this->expectException(required_capability_exception::class);
update_slots::execute($quizobj->get_quizid(), []);
}
/**
* Create a quiz of two shortanswer questions.
*
* @return quiz_settings the newly created quiz.
*/
protected function create_quiz_with_two_shortanswer_questions(): quiz_settings {
global $SITE;
$this->resetAfterTest();
// Make a quiz.
$timeclose = time() + HOURSECS;
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance([
'course' => $SITE->id,
'timeclose' => $timeclose,
'overduehandling' => 'autoabandon',
]);
// Create a question.
/** @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]);
// Add them to the quiz.
$quizobj = quiz_settings::create($quiz->id);
quiz_add_quiz_question($saq1->id, $quiz, 0, 1);
quiz_add_quiz_question($saq2->id, $quiz, 0, 1);
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
return $quizobj;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
slot,type,which,cat,mark
1,random,,rand,1
,shortanswer,,rand,1
,numerical,,rand,1
2,calculatedsimple,sumwithvariants,maincat,1
3,match,,maincat,1
4,truefalse,,maincat,1
5,multichoice,two_of_four,maincat,1
6,multichoice,one_of_four,maincat,1
7,multianswer,,maincat,1
1 slot type which cat mark
2 1 random rand 1
3 shortanswer rand 1
4 numerical rand 1
5 2 calculatedsimple sumwithvariants maincat 1
6 3 match maincat 1
7 4 truefalse maincat 1
8 5 multichoice two_of_four maincat 1
9 6 multichoice one_of_four maincat 1
10 7 multianswer maincat 1
+2
View File
@@ -0,0 +1,2 @@
slot,type,which,cat,mark,overrides.penalty
1,multianswer,,maincat,100,0.3333333
1 slot type which cat mark overrides.penalty
2 1 multianswer maincat 100 0.3333333
+3
View File
@@ -0,0 +1,3 @@
testnumber,preferredbehaviour
00,deferredfeedback
01,interactive
1 testnumber preferredbehaviour
2 00 deferredfeedback
3 01 interactive
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
quizattempt,slots.1.mark,slots.2.mark,slots.3.mark,slots.4.mark,slots.5.mark,slots.6.mark,slots.7.mark,summarks
1,0.8,1,1,1,0.5,1,0,5.3
2,1,0,1,1,0.5,0,0.5,4
3,0,1,1,0,0.5,0,0.5,3
4,0,1,0.3333333,1,1,1,1,5.33333
5,1,1,1,1,0.5,1,1,6.5
6,0,1,1,1,0.5,1,1,5.5
7,0,0,1,0,0.5,0,0,1.5
8,0.8,1,1,1,0.5,1,0,5.3
9,0,0,0.3333333,0,0.5,1,1,2.83333
10,0.8,1,1,1,0.5,1,0.5,5.8
1 quizattempt slots.1.mark slots.2.mark slots.3.mark slots.4.mark slots.5.mark slots.6.mark slots.7.mark summarks
2 1 0.8 1 1 1 0.5 1 0 5.3
3 2 1 0 1 1 0.5 0 0.5 4
4 3 0 1 1 0 0.5 0 0.5 3
5 4 0 1 0.3333333 1 1 1 1 5.33333
6 5 1 1 1 1 0.5 1 1 6.5
7 6 0 1 1 1 0.5 1 1 5.5
8 7 0 0 1 0 0.5 0 0 1.5
9 8 0.8 1 1 1 0.5 1 0 5.3
10 9 0 0 0.3333333 0 0.5 1 1 2.83333
11 10 0.8 1 1 1 0.5 1 0.5 5.8
+9
View File
@@ -0,0 +1,9 @@
quizattempt,finished,slots.1.mark,summarks,attemptnumber
1,1,33.33334,33.33334,1
2,1,50,50,1
3,1,50,50,1
4,0,100,!NULL!,1
5,1,33.33334,33.33334,2
6,1,50,50,2
7,1,50,50,2
8,0,100,!NULL!,2
1 quizattempt finished slots.1.mark summarks attemptnumber
2 1 1 33.33334 33.33334 1
3 2 1 50 50 1
4 3 1 50 50 1
5 4 0 100 !NULL! 1
6 5 1 33.33334 33.33334 2
7 6 1 50 50 2
8 7 1 50 50 2
9 8 0 100 !NULL! 2
+11
View File
@@ -0,0 +1,11 @@
quizattempt,firstname,lastname,randqs.1,responses.1.answer,variants.2,responses.2.answer,responses.3.frog,responses.3.cat,responses.3.newt,responses.4.answer,responses.5.One,responses.5.Two,responses.5.Three,responses.5.Four,responses.6.answer,responses.7.1.answer,responses.7.2.answer
1,John,Jones,shortanswer,toad,4,19.4,amphibian,mammal,amphibian,1,0,1,1,0,One,Pussy-cat,Bow-wow
2,John,Smith,shortanswer,frog,6,-0.6,amphibian,mammal,amphibian,1,1,1,0,0,Four,Dog,Pussy-cat
3,John,Vicars,numerical,3.142,4,19.4,amphibian,mammal,amphibian,0,0,0,1,1,Two,Owl,"Wiggly worm"
4,John,Pacino,numerical,3.142,6,9.4,mammal,amphibian,amphibian,1,1,0,1,0,One,Owl,Pussy-cat
5,John,Deniro,shortanswer,frog,9,7.1,amphibian,mammal,amphibian,1,0,0,1,1,One,Owl,Pussy-cat
6,John,Banks,numerical,3.1,7,9.1,amphibian,mammal,amphibian,1,1,0,0,1,One,Owl,Pussy-cat
7,John,Asimov,numerical,2.5,3,-0.2,amphibian,mammal,amphibian,0,1,1,0,0,Two,Dog,"Wiggly worm"
8,John,Chomsky,shortanswer,toad,2,8.5,amphibian,mammal,amphibian,1,0,0,1,1,One,Dog,"Wiggly worm"
9,John,Yamaguchi,shortanswer,tadpole,5,-0.1,amphibian,amphibian,mammal,0,1,1,0,0,One,Owl,Pussy-cat
10,John,Robbins,shortanswer,toad,7,9.1,amphibian,mammal,amphibian,1,1,0,0,1,One,Owl,"Wiggly worm"
1 quizattempt firstname lastname randqs.1 responses.1.answer variants.2 responses.2.answer responses.3.frog responses.3.cat responses.3.newt responses.4.answer responses.5.One responses.5.Two responses.5.Three responses.5.Four responses.6.answer responses.7.1.answer responses.7.2.answer
2 1 John Jones shortanswer toad 4 19.4 amphibian mammal amphibian 1 0 1 1 0 One Pussy-cat Bow-wow
3 2 John Smith shortanswer frog 6 -0.6 amphibian mammal amphibian 1 1 1 0 0 Four Dog Pussy-cat
4 3 John Vicars numerical 3.142 4 19.4 amphibian mammal amphibian 0 0 0 1 1 Two Owl Wiggly worm
5 4 John Pacino numerical 3.142 6 9.4 mammal amphibian amphibian 1 1 0 1 0 One Owl Pussy-cat
6 5 John Deniro shortanswer frog 9 7.1 amphibian mammal amphibian 1 0 0 1 1 One Owl Pussy-cat
7 6 John Banks numerical 3.1 7 9.1 amphibian mammal amphibian 1 1 0 0 1 One Owl Pussy-cat
8 7 John Asimov numerical 2.5 3 -0.2 amphibian mammal amphibian 0 1 1 0 0 Two Dog Wiggly worm
9 8 John Chomsky shortanswer toad 2 8.5 amphibian mammal amphibian 1 0 0 1 1 One Dog Wiggly worm
10 9 John Yamaguchi shortanswer tadpole 5 -0.1 amphibian amphibian mammal 0 1 1 0 0 One Owl Pussy-cat
11 10 John Robbins shortanswer toad 7 9.1 amphibian mammal amphibian 1 1 0 0 1 One Owl Wiggly worm
+21
View File
@@ -0,0 +1,21 @@
quizattempt,firstname,lastname,responses.1.1.answer,responses.1.2.answer,responses.1.-submit,responses.1.-tryagain,finished
1,Juniper,Jones,Pussy-cat,Bow-wow,1,0,0
1,Juniper,Jones,Owl,Bow-wow,0,1,0
1,Juniper,Jones,Owl,Bow-wow,1,0,1
2,Juniper,Smith,Dog,Pussy-cat,1,0,0
2,Juniper,Smith,Dog,Pussy-cat,0,1,0
2,Juniper,Smith,Dog,Pussy-cat,1,0,1
3,Juniper,Vicars,Owl,"Wiggly worm",1,0,0
3,Juniper,Vicars,Owl,"Wiggly worm",1,0,0
3,Juniper,Vicars,Owl,"Wiggly worm",1,0,1
4,Juniper,Pacino,Owl,Pussy-cat,1,0,0
5,Juniper,Jones,Pussy-cat,Bow-wow,1,0,0
5,Juniper,Jones,Owl,Bow-wow,0,1,0
5,Juniper,Jones,Owl,Bow-wow,1,0,1
6,Juniper,Smith,Dog,Pussy-cat,1,0,0
6,Juniper,Smith,Dog,Pussy-cat,0,1,0
6,Juniper,Smith,Dog,Pussy-cat,1,0,1
7,Juniper,Vicars,Owl,"Wiggly worm",1,0,0
7,Juniper,Vicars,Owl,"Wiggly worm",1,0,0
7,Juniper,Vicars,Owl,"Wiggly worm",1,0,1
8,Juniper,Pacino,Owl,Pussy-cat,1,0,0
1 quizattempt firstname lastname responses.1.1.answer responses.1.2.answer responses.1.-submit responses.1.-tryagain finished
2 1 Juniper Jones Pussy-cat Bow-wow 1 0 0
3 1 Juniper Jones Owl Bow-wow 0 1 0
4 1 Juniper Jones Owl Bow-wow 1 0 1
5 2 Juniper Smith Dog Pussy-cat 1 0 0
6 2 Juniper Smith Dog Pussy-cat 0 1 0
7 2 Juniper Smith Dog Pussy-cat 1 0 1
8 3 Juniper Vicars Owl Wiggly worm 1 0 0
9 3 Juniper Vicars Owl Wiggly worm 1 0 0
10 3 Juniper Vicars Owl Wiggly worm 1 0 1
11 4 Juniper Pacino Owl Pussy-cat 1 0 0
12 5 Juniper Jones Pussy-cat Bow-wow 1 0 0
13 5 Juniper Jones Owl Bow-wow 0 1 0
14 5 Juniper Jones Owl Bow-wow 1 0 1
15 6 Juniper Smith Dog Pussy-cat 1 0 0
16 6 Juniper Smith Dog Pussy-cat 0 1 0
17 6 Juniper Smith Dog Pussy-cat 1 0 1
18 7 Juniper Vicars Owl Wiggly worm 1 0 0
19 7 Juniper Vicars Owl Wiggly worm 1 0 0
20 7 Juniper Vicars Owl Wiggly worm 1 0 1
21 8 Juniper Pacino Owl Pussy-cat 1 0 0
@@ -0,0 +1,76 @@
<?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/>.
/**
* Behat data generator for mod_quiz.
*
* @package mod_quiz
* @category test
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Behat data generator for mod_quiz.
*
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_mod_quiz_generator extends behat_generator_base {
protected function get_creatable_entities(): array {
return [
'group overrides' => [
'singular' => 'group override',
'datagenerator' => 'override',
'required' => ['quiz', 'group'],
'switchids' => ['quiz' => 'quiz', 'group' => 'groupid'],
],
'user overrides' => [
'singular' => 'user override',
'datagenerator' => 'override',
'required' => ['quiz', 'user'],
'switchids' => ['quiz' => 'quiz', 'user' => 'userid'],
],
'grade items' => [
'singular' => 'grade item',
'datagenerator' => 'grade_item',
'required' => ['quiz', 'name'],
'switchids' => ['quiz' => 'quizid'],
],
];
}
/**
* Look up the id of a quiz from its name.
*
* @param string $quizname the quiz name, for example 'Test quiz'.
* @return int corresponding id.
*/
protected function get_quiz_id(string $quizname): int {
global $DB;
if (!$id = $DB->get_field('quiz', 'id', ['name' => $quizname])) {
throw new Exception('There is no quiz with name "' . $quizname . '" does not exist');
}
return $id;
}
}
+247
View File
@@ -0,0 +1,247 @@
<?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/>.
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
/**
* Quiz module test data generator class
*
* @package mod_quiz
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_quiz_generator extends testing_module_generator {
public function create_instance($record = null, array $options = null) {
global $CFG;
require_once($CFG->dirroot.'/mod/quiz/locallib.php');
$record = (object)(array)$record;
$defaultquizsettings = [
'timeopen' => 0,
'timeclose' => 0,
'preferredbehaviour' => 'deferredfeedback',
'attempts' => 0,
'attemptonlast' => 0,
'grademethod' => QUIZ_GRADEHIGHEST,
'decimalpoints' => 2,
'questiondecimalpoints' => -1,
'attemptduring' => 1,
'correctnessduring' => 1,
'maxmarksduring' => 1,
'marksduring' => 1,
'specificfeedbackduring' => 1,
'generalfeedbackduring' => 1,
'rightanswerduring' => 1,
'overallfeedbackduring' => 0,
'attemptimmediately' => 1,
'correctnessimmediately' => 1,
'maxmarksimmediately' => 1,
'marksimmediately' => 1,
'specificfeedbackimmediately' => 1,
'generalfeedbackimmediately' => 1,
'rightanswerimmediately' => 1,
'overallfeedbackimmediately' => 1,
'attemptopen' => 1,
'correctnessopen' => 1,
'maxmarksopen' => 1,
'marksopen' => 1,
'specificfeedbackopen' => 1,
'generalfeedbackopen' => 1,
'rightansweropen' => 1,
'overallfeedbackopen' => 1,
'attemptclosed' => 1,
'correctnessclosed' => 1,
'maxmarksclosed' => 1,
'marksclosed' => 1,
'specificfeedbackclosed' => 1,
'generalfeedbackclosed' => 1,
'rightanswerclosed' => 1,
'overallfeedbackclosed' => 1,
'questionsperpage' => 1,
'shuffleanswers' => 1,
'sumgrades' => 0,
'grade' => 100,
'timecreated' => time(),
'timemodified' => time(),
'timelimit' => 0,
'overduehandling' => 'autosubmit',
'graceperiod' => 86400,
'quizpassword' => '',
'subnet' => '',
'browsersecurity' => '',
'delay1' => 0,
'delay2' => 0,
'showuserpicture' => 0,
'showblocks' => 0,
'navmethod' => QUIZ_NAVMETHOD_FREE,
];
foreach ($defaultquizsettings as $name => $value) {
if (!isset($record->{$name})) {
$record->{$name} = $value;
}
}
if (isset($record->gradepass)) {
$record->gradepass = unformat_float($record->gradepass);
}
return parent::create_instance($record, (array)$options);
}
/**
* Create a quiz attempt for a particular user at a particular course.
*
* @param int $quizid the quiz id (from the mdl_quit table, not cmid).
* @param int $userid the user id.
* @param array $forcedrandomquestions slot => questionid. Optional,
* used with random questions, to control which one is 'randomly' selected in that slot.
* @param array $forcedvariants slot => variantno. Optional. Optional,
* used with question where get_num_variants is > 1, to control which
* variants is 'randomly' selected.
* @return stdClass the new attempt.
*/
public function create_attempt($quizid, $userid, array $forcedrandomquestions = [],
array $forcedvariants = []) {
// Build quiz object and load questions.
$quizobj = quiz_settings::create($quizid, $userid);
$attemptnumber = 1;
$attempt = null;
if ($attempts = quiz_get_user_attempts($quizid, $userid, 'all', true)) {
// There is/are already an attempt/some attempts.
// Take the last attempt.
$attempt = end($attempts);
// Take the attempt number of the last attempt and increase it.
$attemptnumber = $attempt->attempt + 1;
}
return quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $attempt, false,
$forcedrandomquestions, $forcedvariants);
}
/**
* Submit responses to a quiz attempt.
*
* To be realistic, you should ensure that $USER is set to the user whose attempt
* it is before calling this.
*
* @param int $attemptid the id of the attempt which is being
* @param array $responses array responses to submit. See description on
* {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}.
* @param bool $checkbutton if simulate a click on the check button for each question, else simulate save.
* This should only be used with behaviours that have a check button.
* @param bool $finishattempt if true, the attempt will be submitted.
*/
public function submit_responses($attemptid, array $responses, $checkbutton, $finishattempt) {
$questiongenerator = $this->datagenerator->get_plugin_generator('core_question');
$attemptobj = quiz_attempt::create($attemptid);
$postdata = $questiongenerator->get_simulated_post_data_for_questions_in_usage(
$attemptobj->get_question_usage(), $responses, $checkbutton);
$attemptobj->process_submitted_actions(time(), false, $postdata);
// Bit if a hack for interactive behaviour.
// TODO handle this in a more plugin-friendly way.
if ($checkbutton) {
$postdata = [];
foreach ($responses as $slot => $notused) {
$qa = $attemptobj->get_question_attempt($slot);
if ($qa->get_behaviour() instanceof qbehaviour_interactive && $qa->get_behaviour()->is_try_again_state()) {
$postdata[$qa->get_control_field_name('sequencecheck')] = (string)$qa->get_sequence_check_count();
$postdata[$qa->get_flag_field_name()] = (string)(int)$qa->is_flagged();
$postdata[$qa->get_behaviour_field_name('tryagain')] = 1;
}
}
if ($postdata) {
$attemptobj->process_submitted_actions(time(), false, $postdata);
}
}
if ($finishattempt) {
$attemptobj->process_finish(time(), false);
}
}
/**
* Create a quiz override (either user or group).
*
* @param array $data must specify quizid, and one of userid or groupid.
*/
public function create_override(array $data): void {
global $DB;
// Validate.
if (!isset($data['quiz'])) {
throw new coding_exception('Must specify quiz (id) when creating a quiz override.');
}
if (!isset($data['userid']) && !isset($data['groupid'])) {
throw new coding_exception('Must specify one of userid or groupid when creating a quiz override.');
}
if (isset($data['userid']) && isset($data['groupid'])) {
throw new coding_exception('Cannot specify both userid and groupid when creating a quiz override.');
}
// Create the override.
$DB->insert_record('quiz_overrides', (object) $data);
// Update any associated calendar events, if necessary.
quiz_update_events($DB->get_record('quiz', ['id' => $data['quiz']], '*', MUST_EXIST));
}
/**
* Create a quiz override (either user or group).
*
* @param array $data must specify quizid and a name.
* @return stdClass the newly created quiz_grade_items row.
*/
public function create_grade_item(array $data): stdClass {
global $DB;
// Validate.
if (!isset($data['quizid'])) {
throw new coding_exception('Must specify quizid when creating a quiz grade item.');
}
if (!isset($data['name'])) {
throw new coding_exception('Must specify a name when creating a quiz grade item.');
}
if (clean_param($data['name'], PARAM_TEXT) !== $data['name']) {
throw new coding_exception('Grade item name must be PARAM_TEXT.');
}
$data['sortorder'] = $DB->get_field('quiz_grade_items',
'COALESCE(MAX(sortorder) + 1, 1)',
['quizid' => $data['quizid']]);
// Create the grade item.
$gradeitem = (object) $data;
$gradeitem->id = $DB->insert_record('quiz_grade_items', $gradeitem);
return $gradeitem;
}
}
+141
View File
@@ -0,0 +1,141 @@
<?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;
/**
* PHPUnit data generator testcase
*
* @package mod_quiz
* @category phpunit
* @copyright 2012 Matt Petro
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz_generator
*/
class generator_test extends \advanced_testcase {
public function test_generator(): void {
global $DB, $SITE;
$this->resetAfterTest(true);
$this->assertEquals(0, $DB->count_records('quiz'));
/** @var \mod_quiz_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$this->assertInstanceOf('mod_quiz_generator', $generator);
$this->assertEquals('quiz', $generator->get_modulename());
$generator->create_instance(['course' => $SITE->id]);
$generator->create_instance(['course' => $SITE->id]);
$createtime = time();
$quiz = $generator->create_instance(['course' => $SITE->id, 'timecreated' => 0]);
$this->assertEquals(3, $DB->count_records('quiz'));
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
$this->assertEquals($quiz->id, $cm->instance);
$this->assertEquals('quiz', $cm->modname);
$this->assertEquals($SITE->id, $cm->course);
$context = \context_module::instance($cm->id);
$this->assertEquals($quiz->cmid, $context->instanceid);
$this->assertEqualsWithDelta($createtime,
$DB->get_field('quiz', 'timecreated', ['id' => $cm->instance]), 2);
}
public function test_generating_a_user_override(): void {
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$user = $generator->create_user();
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
$generator->enrol_user($user->id, $course->id, 'student');
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quizgenerator->create_override([
'quiz' => $quiz->id,
'userid' => $user->id,
'timeclose' => strtotime('2022-10-20'),
]);
// Check the corresponding calendar event now exists.
$events = calendar_get_events(strtotime('2022-01-01'),
strtotime('2022-12-31'), $user->id, false, $course->id);
$this->assertCount(1, $events);
$event = reset($events);
$this->assertEquals($user->id, $event->userid);
$this->assertEquals(0, $event->groupid);
$this->assertEquals(0, $event->courseid);
$this->assertEquals('quiz', $event->modulename);
$this->assertEquals($quiz->id, $event->instance);
$this->assertEquals('close', $event->eventtype);
$this->assertEquals(strtotime('2022-10-20'), $event->timestart);
}
public function test_generating_a_group_override(): void {
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
$group = $generator->create_group(['courseid' => $course->id]);
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quizgenerator->create_override([
'quiz' => $quiz->id,
'groupid' => $group->id,
'timeclose' => strtotime('2022-10-20'),
]);
// Check the corresponding calendar event now exists.
$events = calendar_get_events(strtotime('2022-01-01'),
strtotime('2022-12-31'), false, $group->id, $course->id);
$this->assertCount(1, $events);
$event = reset($events);
$this->assertEquals(0, $event->userid);
$this->assertEquals($group->id, $event->groupid);
$this->assertEquals($course->id, $event->courseid);
$this->assertEquals('quiz', $event->modulename);
$this->assertEquals($quiz->id, $event->instance);
$this->assertEquals('close', $event->eventtype);
$this->assertEquals(strtotime('2022-10-20'), $event->timestart);
}
public function test_generating_a_grade_item(): void {
$this->resetAfterTest();
// Create a quiz to use in the test.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
// Create a grade item.
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$newgradeitem = $quizgenerator->create_grade_item([
'quizid' => $quiz->id,
'name' => 'Awesomeness!',
]);
// Verify the grade item was created correctly.
$this->assertObjectHasProperty('id', $newgradeitem);
$this->assertEquals($quiz->id, $newgradeitem->quizid);
$this->assertEquals('Awesomeness!', $newgradeitem->name);
}
}
+987
View File
@@ -0,0 +1,987 @@
<?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/>.
/**
* Unit tests for (some of) mod/quiz/locallib.php.
*
* @package mod_quiz
* @category test
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
namespace mod_quiz;
use context_module;
use core_external\external_api;
use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/lib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
class lib_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
public function test_quiz_has_grades(): void {
$quiz = new \stdClass();
$quiz->grade = '100.0000';
$quiz->sumgrades = '100.0000';
$this->assertTrue(quiz_has_grades($quiz));
$quiz->sumgrades = '0.0000';
$this->assertFalse(quiz_has_grades($quiz));
$quiz->grade = '0.0000';
$this->assertFalse(quiz_has_grades($quiz));
$quiz->sumgrades = '100.0000';
$this->assertFalse(quiz_has_grades($quiz));
}
public function test_quiz_format_grade(): void {
$quiz = new \stdClass();
$quiz->decimalpoints = 2;
$this->assertEquals(quiz_format_grade($quiz, 0.12345678), format_float(0.12, 2));
$this->assertEquals(quiz_format_grade($quiz, 0), format_float(0, 2));
$this->assertEquals(quiz_format_grade($quiz, 1.000000000000), format_float(1, 2));
$quiz->decimalpoints = 0;
$this->assertEquals(quiz_format_grade($quiz, 0.12345678), '0');
}
public function test_quiz_get_grade_format(): void {
$quiz = new \stdClass();
$quiz->decimalpoints = 2;
$this->assertEquals(quiz_get_grade_format($quiz), 2);
$this->assertEquals($quiz->questiondecimalpoints, -1);
$quiz->questiondecimalpoints = 2;
$this->assertEquals(quiz_get_grade_format($quiz), 2);
$quiz->decimalpoints = 3;
$quiz->questiondecimalpoints = -1;
$this->assertEquals(quiz_get_grade_format($quiz), 3);
$quiz->questiondecimalpoints = 4;
$this->assertEquals(quiz_get_grade_format($quiz), 4);
}
public function test_quiz_format_question_grade(): void {
$quiz = new \stdClass();
$quiz->decimalpoints = 2;
$quiz->questiondecimalpoints = 2;
$this->assertEquals(quiz_format_question_grade($quiz, 0.12345678), format_float(0.12, 2));
$this->assertEquals(quiz_format_question_grade($quiz, 0), format_float(0, 2));
$this->assertEquals(quiz_format_question_grade($quiz, 1.000000000000), format_float(1, 2));
$quiz->decimalpoints = 3;
$quiz->questiondecimalpoints = -1;
$this->assertEquals(quiz_format_question_grade($quiz, 0.12345678), format_float(0.123, 3));
$this->assertEquals(quiz_format_question_grade($quiz, 0), format_float(0, 3));
$this->assertEquals(quiz_format_question_grade($quiz, 1.000000000000), format_float(1, 3));
$quiz->questiondecimalpoints = 4;
$this->assertEquals(quiz_format_question_grade($quiz, 0.12345678), format_float(0.1235, 4));
$this->assertEquals(quiz_format_question_grade($quiz, 0), format_float(0, 4));
$this->assertEquals(quiz_format_question_grade($quiz, 1.000000000000), format_float(1, 4));
}
/**
* Test deleting a quiz instance.
*/
public function test_quiz_delete_instance(): void {
global $SITE, $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
// Setup a quiz with 1 standard and 1 random question.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0]);
$context = context_module::instance($quiz->cmid);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$standardq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($standardq->id, $quiz);
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
// Get the random question.
$randomq = $DB->get_record('question', ['qtype' => 'random']);
quiz_delete_instance($quiz->id);
// Check that the random question was deleted.
if ($randomq) {
$this->assertEquals(0, $DB->count_records('question', ['id' => $randomq->id]));
}
// Check that the standard question was not deleted.
$this->assertEquals(1, $DB->count_records('question', ['id' => $standardq->id]));
// Check that all the slots were removed.
$this->assertEquals(0, $DB->count_records('quiz_slots', ['quizid' => $quiz->id]));
// Check that the quiz was removed.
$this->assertEquals(0, $DB->count_records('quiz', ['id' => $quiz->id]));
// Check that any question references linked to this quiz are gone.
$this->assertEquals(0, $DB->count_records('question_references', ['usingcontextid' => $context->id]));
$this->assertEquals(0, $DB->count_records('question_set_references', ['usingcontextid' => $context->id]));
}
public function test_quiz_get_user_attempts(): void {
global $DB;
$this->resetAfterTest();
$dg = $this->getDataGenerator();
$quizgen = $dg->get_plugin_generator('mod_quiz');
$course = $dg->create_course();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u3 = $dg->create_user();
$u4 = $dg->create_user();
$role = $DB->get_record('role', ['shortname' => 'student']);
$dg->enrol_user($u1->id, $course->id, $role->id);
$dg->enrol_user($u2->id, $course->id, $role->id);
$dg->enrol_user($u3->id, $course->id, $role->id);
$dg->enrol_user($u4->id, $course->id, $role->id);
$quiz1 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
$quiz2 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
// Questions.
$questgen = $dg->get_plugin_generator('core_question');
$quizcat = $questgen->create_question_category();
$question = $questgen->create_question('numerical', null, ['category' => $quizcat->id]);
quiz_add_quiz_question($question->id, $quiz1);
quiz_add_quiz_question($question->id, $quiz2);
$quizobj1a = quiz_settings::create($quiz1->id, $u1->id);
$quizobj1b = quiz_settings::create($quiz1->id, $u2->id);
$quizobj1c = quiz_settings::create($quiz1->id, $u3->id);
$quizobj1d = quiz_settings::create($quiz1->id, $u4->id);
$quizobj2a = quiz_settings::create($quiz2->id, $u1->id);
// Set attempts.
$quba1a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1a->get_context());
$quba1a->set_preferred_behaviour($quizobj1a->get_quiz()->preferredbehaviour);
$quba1b = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1b->get_context());
$quba1b->set_preferred_behaviour($quizobj1b->get_quiz()->preferredbehaviour);
$quba1c = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1c->get_context());
$quba1c->set_preferred_behaviour($quizobj1c->get_quiz()->preferredbehaviour);
$quba1d = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1d->get_context());
$quba1d->set_preferred_behaviour($quizobj1d->get_quiz()->preferredbehaviour);
$quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
$quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
$timenow = time();
// User 1 passes quiz 1.
$attempt = quiz_create_attempt($quizobj1a, 1, false, $timenow, false, $u1->id);
quiz_start_new_attempt($quizobj1a, $quba1a, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj1a, $quba1a, $attempt);
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
$attemptobj->process_finish($timenow, false);
// User 2 goes overdue in quiz 1.
$attempt = quiz_create_attempt($quizobj1b, 1, false, $timenow, false, $u2->id);
quiz_start_new_attempt($quizobj1b, $quba1b, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj1b, $quba1b, $attempt);
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_going_overdue($timenow, true);
// User 3 does not finish quiz 1.
$attempt = quiz_create_attempt($quizobj1c, 1, false, $timenow, false, $u3->id);
quiz_start_new_attempt($quizobj1c, $quba1c, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj1c, $quba1c, $attempt);
// User 4 abandons the quiz 1.
$attempt = quiz_create_attempt($quizobj1d, 1, false, $timenow, false, $u4->id);
quiz_start_new_attempt($quizobj1d, $quba1d, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj1d, $quba1d, $attempt);
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_abandon($timenow, true);
// User 1 attempts the quiz three times (abandon, finish, in progress).
$quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
$quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
$attempt = quiz_create_attempt($quizobj2a, 1, false, $timenow, false, $u1->id);
quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_abandon($timenow, true);
$quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
$quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
$attempt = quiz_create_attempt($quizobj2a, 2, false, $timenow, false, $u1->id);
quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 2, $timenow);
quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_finish($timenow, false);
$quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
$quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
$attempt = quiz_create_attempt($quizobj2a, 3, false, $timenow, false, $u1->id);
quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 3, $timenow);
quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
// Check for user 1.
$attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'all');
$this->assertCount(1, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
$this->assertEquals($u1->id, $attempt->userid);
$this->assertEquals($quiz1->id, $attempt->quiz);
$attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'finished');
$this->assertCount(1, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
$this->assertEquals($u1->id, $attempt->userid);
$this->assertEquals($quiz1->id, $attempt->quiz);
$attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'unfinished');
$this->assertCount(0, $attempts);
// Check for user 2.
$attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'all');
$this->assertCount(1, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
$this->assertEquals($u2->id, $attempt->userid);
$this->assertEquals($quiz1->id, $attempt->quiz);
$attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'finished');
$this->assertCount(0, $attempts);
$attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'unfinished');
$this->assertCount(1, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
$this->assertEquals($u2->id, $attempt->userid);
$this->assertEquals($quiz1->id, $attempt->quiz);
// Check for user 3.
$attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'all');
$this->assertCount(1, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
$this->assertEquals($u3->id, $attempt->userid);
$this->assertEquals($quiz1->id, $attempt->quiz);
$attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'finished');
$this->assertCount(0, $attempts);
$attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'unfinished');
$this->assertCount(1, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
$this->assertEquals($u3->id, $attempt->userid);
$this->assertEquals($quiz1->id, $attempt->quiz);
// Check for user 4.
$attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'all');
$this->assertCount(1, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
$this->assertEquals($u4->id, $attempt->userid);
$this->assertEquals($quiz1->id, $attempt->quiz);
$attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'finished');
$this->assertCount(1, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
$this->assertEquals($u4->id, $attempt->userid);
$this->assertEquals($quiz1->id, $attempt->quiz);
$attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'unfinished');
$this->assertCount(0, $attempts);
// Multiple attempts for user 1 in quiz 2.
$attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'all');
$this->assertCount(3, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
$this->assertEquals($u1->id, $attempt->userid);
$this->assertEquals($quiz2->id, $attempt->quiz);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
$this->assertEquals($u1->id, $attempt->userid);
$this->assertEquals($quiz2->id, $attempt->quiz);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
$this->assertEquals($u1->id, $attempt->userid);
$this->assertEquals($quiz2->id, $attempt->quiz);
$attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'finished');
$this->assertCount(2, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
$attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'unfinished');
$this->assertCount(1, $attempts);
$attempt = array_shift($attempts);
// Multiple quiz attempts fetched at once.
$attempts = quiz_get_user_attempts([$quiz1->id, $quiz2->id], $u1->id, 'all');
$this->assertCount(4, $attempts);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
$this->assertEquals($u1->id, $attempt->userid);
$this->assertEquals($quiz1->id, $attempt->quiz);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
$this->assertEquals($u1->id, $attempt->userid);
$this->assertEquals($quiz2->id, $attempt->quiz);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
$this->assertEquals($u1->id, $attempt->userid);
$this->assertEquals($quiz2->id, $attempt->quiz);
$attempt = array_shift($attempts);
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
$this->assertEquals($u1->id, $attempt->userid);
$this->assertEquals($quiz2->id, $attempt->quiz);
}
/**
* Test for quiz_get_group_override_priorities().
*/
public function test_quiz_get_group_override_priorities(): void {
global $DB;
$this->resetAfterTest();
$dg = $this->getDataGenerator();
$quizgen = $dg->get_plugin_generator('mod_quiz');
$course = $dg->create_course();
$quiz = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
$this->assertNull(quiz_get_group_override_priorities($quiz->id));
$group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$now = 100;
$override1 = (object)[
'quiz' => $quiz->id,
'groupid' => $group1->id,
'timeopen' => $now,
'timeclose' => $now + 20
];
$DB->insert_record('quiz_overrides', $override1);
$override2 = (object)[
'quiz' => $quiz->id,
'groupid' => $group2->id,
'timeopen' => $now - 10,
'timeclose' => $now + 10
];
$DB->insert_record('quiz_overrides', $override2);
$priorities = quiz_get_group_override_priorities($quiz->id);
$this->assertNotEmpty($priorities);
$openpriorities = $priorities['open'];
// Override 2's time open has higher priority since it is sooner than override 1's.
$this->assertEquals(2, $openpriorities[$override1->timeopen]);
$this->assertEquals(1, $openpriorities[$override2->timeopen]);
$closepriorities = $priorities['close'];
// Override 1's time close has higher priority since it is later than override 2's.
$this->assertEquals(1, $closepriorities[$override1->timeclose]);
$this->assertEquals(2, $closepriorities[$override2->timeclose]);
}
public function test_quiz_core_calendar_provide_event_action_open(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a student and enrol into the course.
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS]);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
// Now, log in as student.
$this->setUser($student);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event.
$actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory);
// Confirm the event was decorated.
$this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
$this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
$this->assertInstanceOf('moodle_url', $actionevent->get_url());
$this->assertEquals(1, $actionevent->get_item_count());
$this->assertTrue($actionevent->is_actionable());
}
public function test_quiz_core_calendar_provide_event_action_open_for_user(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a student and enrol into the course.
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS]);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event for the student.
$actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id);
// Confirm the event was decorated.
$this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
$this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
$this->assertInstanceOf('moodle_url', $actionevent->get_url());
$this->assertEquals(1, $actionevent->get_item_count());
$this->assertTrue($actionevent->is_actionable());
}
public function test_quiz_core_calendar_provide_event_action_closed(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
'timeclose' => time() - DAYSECS]);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Confirm the result was null.
$this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory));
}
public function test_quiz_core_calendar_provide_event_action_closed_for_user(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a student.
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
'timeclose' => time() - DAYSECS]);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Confirm the result was null.
$this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id));
}
public function test_quiz_core_calendar_provide_event_action_open_in_future(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a student and enrol into the course.
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
'timeopen' => time() + DAYSECS]);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
// Now, log in as student.
$this->setUser($student);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event.
$actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory);
// Confirm the event was decorated.
$this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
$this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
$this->assertInstanceOf('moodle_url', $actionevent->get_url());
$this->assertEquals(1, $actionevent->get_item_count());
$this->assertFalse($actionevent->is_actionable());
}
public function test_quiz_core_calendar_provide_event_action_open_in_future_for_user(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a student and enrol into the course.
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
'timeopen' => time() + DAYSECS]);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event for the student.
$actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id);
// Confirm the event was decorated.
$this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
$this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
$this->assertInstanceOf('moodle_url', $actionevent->get_url());
$this->assertEquals(1, $actionevent->get_item_count());
$this->assertFalse($actionevent->is_actionable());
}
public function test_quiz_core_calendar_provide_event_action_no_capability(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a student.
$student = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
// Enrol student.
$this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
// Remove the permission to attempt or review the quiz for the student role.
$coursecontext = \context_course::instance($course->id);
assign_capability('mod/quiz:reviewmyattempts', CAP_PROHIBIT, $studentrole->id, $coursecontext);
assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $studentrole->id, $coursecontext);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Set current user to the student.
$this->setUser($student);
// Confirm null is returned.
$this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory));
}
public function test_quiz_core_calendar_provide_event_action_no_capability_for_user(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a student.
$student = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
// Enrol student.
$this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
// Remove the permission to attempt or review the quiz for the student role.
$coursecontext = \context_course::instance($course->id);
assign_capability('mod/quiz:reviewmyattempts', CAP_PROHIBIT, $studentrole->id, $coursecontext);
assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $studentrole->id, $coursecontext);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Confirm null is returned.
$this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id));
}
public function test_quiz_core_calendar_provide_event_action_already_finished(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a student.
$student = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
// Enrol student.
$this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
'sumgrades' => 1]);
// Add a question to the quiz.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
// Get the quiz object.
$quizobj = quiz_settings::create($quiz->id, $student->id);
// Create an attempt for the student in the quiz.
$timenow = time();
$attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $student->id);
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_finish($timenow, false);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Set current user to the student.
$this->setUser($student);
// Confirm null is returned.
$this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory));
}
public function test_quiz_core_calendar_provide_event_action_already_finished_for_user(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a student.
$student = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
// Enrol student.
$this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
// Create a quiz.
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
'sumgrades' => 1]);
// Add a question to the quiz.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
// Get the quiz object.
$quizobj = quiz_settings::create($quiz->id, $student->id);
// Create an attempt for the student in the quiz.
$timenow = time();
$attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $student->id);
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_finish($timenow, false);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Confirm null is returned.
$this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id));
}
public function test_quiz_core_calendar_provide_event_action_already_completed(): void {
$this->resetAfterTest();
set_config('enablecompletion', 1);
$this->setAdminUser();
// Create the activity.
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id],
['completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS]);
// Get some additional data.
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id,
\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
// Mark the activity as completed.
$completion = new \completion_info($course);
$completion->set_module_viewed($cm);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event.
$actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory);
// Ensure result was null.
$this->assertNull($actionevent);
}
public function test_quiz_core_calendar_provide_event_action_already_completed_for_user(): void {
$this->resetAfterTest();
set_config('enablecompletion', 1);
$this->setAdminUser();
// Create the activity.
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id],
['completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS]);
// Enrol a student in the course.
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Get some additional data.
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
// Create a calendar event.
$event = $this->create_action_event($course->id, $quiz->id,
\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
// Mark the activity as completed for the student.
$completion = new \completion_info($course);
$completion->set_module_viewed($cm, $student->id);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event for the student.
$actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id);
// Ensure result was null.
$this->assertNull($actionevent);
}
/**
* Creates an action event.
*
* @param int $courseid
* @param int $instanceid The quiz id.
* @param string $eventtype The event type. eg. QUIZ_EVENT_TYPE_OPEN.
* @return bool|calendar_event
*/
private function create_action_event($courseid, $instanceid, $eventtype) {
$event = new \stdClass();
$event->name = 'Calendar event';
$event->modulename = 'quiz';
$event->courseid = $courseid;
$event->instance = $instanceid;
$event->type = CALENDAR_EVENT_TYPE_ACTION;
$event->eventtype = $eventtype;
$event->timestart = time();
return \calendar_event::create($event);
}
/**
* Test the callback responsible for returning the completion rule descriptions.
* This function should work given either an instance of the module (cm_info), such as when checking the active rules,
* or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type.
*/
public function test_mod_quiz_completion_get_active_rule_descriptions(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 2]);
$quiz1 = $this->getDataGenerator()->create_module('quiz', [
'course' => $course->id,
'completion' => 2,
'completionusegrade' => 1,
'completionpassgrade' => 1,
'completionattemptsexhausted' => 1,
]);
$quiz2 = $this->getDataGenerator()->create_module('quiz', [
'course' => $course->id,
'completion' => 2,
'completionusegrade' => 0
]);
$cm1 = \cm_info::create(get_coursemodule_from_instance('quiz', $quiz1->id));
$cm2 = \cm_info::create(get_coursemodule_from_instance('quiz', $quiz2->id));
// Data for the stdClass input type.
// This type of input would occur when checking the default completion rules for an activity type, where we don't have
// any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
$moddefaults = new \stdClass();
$moddefaults->customdata = ['customcompletionrules' => [
'completionattemptsexhausted' => 1,
]];
$moddefaults->completion = 2;
$activeruledescriptions = [
get_string('completionpassorattemptsexhausteddesc', 'quiz'),
];
$this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($cm1), $activeruledescriptions);
$this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($cm2), []);
$this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
$this->assertEquals(mod_quiz_get_completion_active_rule_descriptions(new \stdClass()), []);
}
/**
* A user who does not have capabilities to add events to the calendar should be able to create a quiz.
*/
public function test_creation_with_no_calendar_capabilities(): void {
$this->resetAfterTest();
$course = self::getDataGenerator()->create_course();
$context = \context_course::instance($course->id);
$user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
$roleid = self::getDataGenerator()->create_role();
self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
$generator = self::getDataGenerator()->get_plugin_generator('mod_quiz');
// Create an instance as a user without the calendar capabilities.
$this->setUser($user);
$time = time();
$params = [
'course' => $course->id,
'timeopen' => $time + 200,
'timeclose' => $time + 2000,
];
$generator->create_instance($params);
}
/**
* Data provider for summarise_response() test cases.
*
* @return array List of data sets (test cases)
*/
public function mod_quiz_inplace_editable_provider(): array {
return [
'set to A1' => [1, 'A1'],
'set with HTML characters' => [2, 'A & &amp; <-:'],
'set to integer' => [3, '3'],
'set to blank' => [4, ''],
'set with Unicode characters' => [1, 'L\'Aina Lluís^'],
'set with Unicode at the truncation point' => [1, '123456789012345碁'],
'set with HTML Char at the truncation point' => [1, '123456789012345>'],
];
}
/**
* Test customised and automated question numbering for a given slot number and customised value.
*
* @dataProvider mod_quiz_inplace_editable_provider
* @param int $slotnumber
* @param string $newvalue
* @covers ::mod_quiz_inplace_editable
*/
public function test_mod_quiz_inplace_editable(int $slotnumber, string $newvalue): void {
global $CFG;
require_once($CFG->dirroot . '/lib/external/externallib.php');
$this->resetAfterTest();
$this->setAdminUser();
$course = self::getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id, 'sumgrades' => 1]);
$cm = get_coursemodule_from_id('quiz', $quiz->cmid);
// Add few questions to the quiz.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
$question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
$question = $questiongenerator->create_question('multichoice', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
$question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
// Create the quiz object.
$quizobj = new quiz_settings($quiz, $cm, $course);
$structure = $quizobj->get_structure();
$slots = $structure->get_slots();
$this->assertEquals(4, count($slots));
$slotid = $structure->get_slot_id_for_slot($slotnumber);
$inplaceeditable = mod_quiz_inplace_editable('slotdisplaynumber', $slotid, $newvalue);
$result = \core_external::update_inplace_editable('mod_quiz', 'slotdisplaynumber', $slotid, $newvalue);
$result = external_api::clean_returnvalue(\core_external::update_inplace_editable_returns(), $result);
$this->assertEquals(count((array) $inplaceeditable), count($result));
$this->assertEquals($slotid, $result['itemid']);
if ($newvalue === '' || is_null($newvalue)) {
// Check against default.
$this->assertEquals($slotnumber, $result['displayvalue']);
$this->assertEquals($slotnumber, $result['value']);
} else {
// Check against the custom number.
$this->assertEquals(s($newvalue), $result['displayvalue']);
$this->assertEquals($newvalue, $result['value']);
}
}
}
@@ -0,0 +1,80 @@
<?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\local;
/**
* Cache manager tests for quiz overrides
*
* @package mod_quiz
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\local\override_cache
*/
final class override_cache_test extends \advanced_testcase {
/**
* Tests CRUD functions of the override_cache
*/
public function test_crud(): void {
// Cache is normally protected, but for testing we reflect it and put test data into it.
$overridecache = new override_cache(0);
$reflection = new \ReflectionClass($overridecache);
$getcache = $reflection->getMethod('get_cache');
$cache = $getcache->invoke($overridecache);
$getuserkey = $reflection->getMethod('get_user_cache_key');
$getgroupkey = $reflection->getMethod('get_group_cache_key');
$dummydata = (object)[
'userid' => 1234,
];
// Set some data.
$cache->set($getuserkey->invoke($overridecache, 123), $dummydata);
$cache->set($getgroupkey->invoke($overridecache, 456), $dummydata);
// Get the data back.
$this->assertEquals($dummydata, $overridecache->get_cached_user_override(123));
$this->assertEquals($dummydata, $overridecache->get_cached_group_override(456));
// Delete.
$overridecache->clear_for_user(123);
$overridecache->clear_for_group(456);
$this->assertEmpty($overridecache->get_cached_user_override(123));
$this->assertEmpty($overridecache->get_cached_group_override(456));
// Put some data back.
$cache->set($getuserkey->invoke($overridecache, 123), $dummydata);
$cache->set($getgroupkey->invoke($overridecache, 456), $dummydata);
// Clear it.
$overridecache->clear_for(123, 456);
$this->assertEmpty($overridecache->get_cached_user_override(123));
$this->assertEmpty($overridecache->get_cached_group_override(456));
// Put some data back.
$cache->set($getuserkey->invoke($overridecache, 123), 'testuser');
$cache->set($getgroupkey->invoke($overridecache, 456), 'testgroup');
// Purge it.
\cache_helper::purge_by_event(override_cache::INVALIDATION_USERDATARESET);
$this->assertEmpty($overridecache->get_cached_user_override(123));
$this->assertEmpty($overridecache->get_cached_group_override(456));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,305 @@
<?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;
defined('MOODLE_INTERNAL') || die();
use mod_quiz\question\bank\qbank_helper;
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Class mod_quiz_local_structure_slot_random_test
* Class for tests related to the {@link \mod_quiz\local\structure\slot_random} class.
*
* @package mod_quiz
* @category test
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\local\structure\slot_random
*/
class local_structure_slot_random_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Constructor test.
*/
public function test_constructor(): void {
global $SITE;
$this->resetAfterTest();
$this->setAdminUser();
// Create a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0]);
// Create a question category in the system context.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
// Create a random question without adding it to a quiz.
// We don't want to use quiz_add_random_questions because that itself, instantiates an object from the slot_random class.
$form = new \stdClass();
$form->category = $category->id . ',' . $category->contextid;
$form->includesubcategories = true;
$form->fromtags = [];
$form->defaultmark = 1;
$form->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN;
$form->stamp = make_unique_id_code();
// Set the filter conditions.
$filtercondition = new \stdClass();
$filtercondition->filters = \question_filter_test_helper::create_filters([$category->id], true);
// Slot data.
$randomslotdata = new \stdClass();
$randomslotdata->quizid = $quiz->id;
$randomslotdata->maxmark = 1;
$randomslotdata->usingcontextid = \context_module::instance($quiz->cmid)->id;
$randomslotdata->questionscontextid = $category->contextid;
// Insert the random question to the quiz.
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_filter_condition(json_encode($filtercondition));
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('filtercondition');
$record = json_decode($rcp->getValue($randomslot));
$this->assertEquals($quiz->id, $randomslot->get_quiz()->id);
$this->assertEquals($category->id, $record->filters->category->values[0]);
$this->assertTrue($record->filters->category->filteroptions->includesubcategories);
$rcp = $rc->getProperty('record');
$record = $rcp->getValue($randomslot);
$this->assertEquals(1, $record->maxmark);
}
public function test_get_quiz_quiz(): void {
global $SITE, $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Create a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0]);
// Create a question category in the system context.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
$this->add_random_questions($quiz->id, 0, $category->id, 1);
// Set the filter conditions.
$filtercondition = new \stdClass();
$filtercondition->filters = \question_filter_test_helper::create_filters([$category->id], 1);
// Slot data.
$randomslotdata = new \stdClass();
$randomslotdata->quizid = $quiz->id;
$randomslotdata->maxmark = 1;
$randomslotdata->usingcontextid = \context_module::instance($quiz->cmid)->id;
$randomslotdata->questionscontextid = $category->contextid;
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_filter_condition(json_encode($filtercondition));
// The create_instance had injected an additional cmid propery to the quiz. Let's remove that.
unset($quiz->cmid);
$this->assertEquals($quiz, $randomslot->get_quiz());
}
public function test_set_quiz(): void {
global $SITE, $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Create a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0]);
// Create a question category in the system context.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
$this->add_random_questions($quiz->id, 0, $category->id, 1);
// Set the filter conditions.
$filtercondition = new \stdClass();
$filtercondition->filters = \question_filter_test_helper::create_filters([$category->id], 1);
// Slot data.
$randomslotdata = new \stdClass();
$randomslotdata->quizid = $quiz->id;
$randomslotdata->maxmark = 1;
$randomslotdata->usingcontextid = \context_module::instance($quiz->cmid)->id;
$randomslotdata->questionscontextid = $category->contextid;
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_filter_condition(json_encode($filtercondition));
// The create_instance had injected an additional cmid propery to the quiz. Let's remove that.
unset($quiz->cmid);
$randomslot->set_quiz($quiz);
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('quiz');
$quizpropery = $rcp->getValue($randomslot);
$this->assertEquals($quiz, $quizpropery);
}
private function setup_for_test_tags($tagnames) {
global $SITE, $DB;
// Create a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0]);
// Create a question category in the system context.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
$this->add_random_questions($quiz->id, 0, $category->id, 1);
// Slot data.
$randomslotdata = new \stdClass();
$randomslotdata->quizid = $quiz->id;
$randomslotdata->maxmark = 1;
$randomslotdata->usingcontextid = \context_module::instance($quiz->cmid)->id;
$randomslotdata->questionscontextid = $category->contextid;
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
// Create tags.
foreach ($tagnames as $tagname) {
$tagrecord = [
'isstandard' => 1,
'flag' => 0,
'rawname' => $tagname,
'description' => $tagname . ' desc'
];
$tags[$tagname] = $this->getDataGenerator()->create_tag($tagrecord);
}
return [$randomslot, $tags];
}
public function test_set_tags_filter(): void {
$this->resetAfterTest();
$this->setAdminUser();
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar']);
$qtagids = [$tags['foo']->id, $tags['bar']->id];
$filtercondition = new \stdClass();
$filtercondition->filters = \question_filter_test_helper::create_filters([], 0, $qtagids);
$randomslot->set_filter_condition(json_encode($filtercondition));
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('filtercondition');
$tagspropery = $rcp->getValue($randomslot);
$this->assertEquals([$tags['foo']->id, $tags['bar']->id],
(array)json_decode($tagspropery)->filters->qtagids->values);
}
public function test_insert(): void {
global $SITE;
$this->resetAfterTest();
$this->setAdminUser();
// Create a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0]);
$quizcontext = \context_module::instance($quiz->cmid);
// Create a question category in the system context.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
// Create a random question without adding it to a quiz.
$form = new \stdClass();
$form->category = $category->id . ',' . $category->contextid;
$form->includesubcategories = true;
$form->fromtags = [];
$form->defaultmark = 1;
$form->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN;
$form->stamp = make_unique_id_code();
// Prepare 2 tags.
$tagrecord = [
'isstandard' => 1,
'flag' => 0,
'rawname' => 'foo',
'description' => 'foo desc'
];
$footag = $this->getDataGenerator()->create_tag($tagrecord);
$tagrecord = [
'isstandard' => 1,
'flag' => 0,
'rawname' => 'bar',
'description' => 'bar desc'
];
$bartag = $this->getDataGenerator()->create_tag($tagrecord);
// Set the filter conditions.
$filtercondition = new \stdClass();
$filtercondition->filter = \question_filter_test_helper::create_filters([$category->id], true, [$footag->id, $bartag->id]);
// Slot data.
$randomslotdata = new \stdClass();
$randomslotdata->quizid = $quiz->id;
$randomslotdata->maxmark = 1;
$randomslotdata->usingcontextid = $quizcontext->id;
$randomslotdata->questionscontextid = $category->contextid;
// Insert the random question to the quiz.
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_filter_condition(json_encode($filtercondition));
$randomslot->insert(1); // Put the question on the first page of the quiz.
$slots = qbank_helper::get_question_structure($quiz->id, $quizcontext);
$quizslot = reset($slots);
$filter = $quizslot->filtercondition['filter'];
$this->assertEquals($category->id, $filter['category']['values'][0]);
$this->assertTrue($filter['category']['filteroptions']['includesubcategories']);
$this->assertEquals(1, $quizslot->maxmark);
$this->assertCount(2, $filter['qtagids']['values']);
$this->assertEqualsCanonicalizing(
[
['tagid' => $footag->id],
['tagid' => $bartag->id]
],
array_map(function($tagid) {
return ['tagid' => $tagid];
}, $filter['qtagids']['values']));
}
}
+688
View File
@@ -0,0 +1,688 @@
<?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/>.
/**
* Unit tests for (some of) mod/quiz/locallib.php.
*
* @package mod_quiz
* @category test
* @copyright 2008 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz;
use mod_quiz\output\renderer;
use mod_quiz\question\display_options;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Unit tests for (some of) mod/quiz/locallib.php.
*
* @copyright 2008 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class locallib_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
public function test_quiz_rescale_grade(): void {
$quiz = new \stdClass();
$quiz->decimalpoints = 2;
$quiz->questiondecimalpoints = 3;
$quiz->grade = 10;
$quiz->sumgrades = 10;
$this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, false), 0.12345678);
$this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, true), format_float(0.12, 2));
$this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, 'question'),
format_float(0.123, 3));
$quiz->sumgrades = 5;
$this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, false), 0.24691356);
$this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, true), format_float(0.25, 2));
$this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, 'question'),
format_float(0.247, 3));
}
public function quiz_attempt_state_data_provider() {
return [
[quiz_attempt::IN_PROGRESS, null, null, display_options::DURING],
[quiz_attempt::FINISHED, -90, null, display_options::IMMEDIATELY_AFTER],
[quiz_attempt::FINISHED, -7200, null, display_options::LATER_WHILE_OPEN],
[quiz_attempt::FINISHED, -7200, 3600, display_options::LATER_WHILE_OPEN],
[quiz_attempt::FINISHED, -30, 30, display_options::IMMEDIATELY_AFTER],
[quiz_attempt::FINISHED, -90, -30, display_options::AFTER_CLOSE],
[quiz_attempt::FINISHED, -7200, -3600, display_options::AFTER_CLOSE],
[quiz_attempt::FINISHED, -90, -3600, display_options::AFTER_CLOSE],
[quiz_attempt::ABANDONED, -10000000, null, display_options::LATER_WHILE_OPEN],
[quiz_attempt::ABANDONED, -7200, 3600, display_options::LATER_WHILE_OPEN],
[quiz_attempt::ABANDONED, -7200, -3600, display_options::AFTER_CLOSE],
];
}
/**
* @dataProvider quiz_attempt_state_data_provider
*
* @param string $attemptstate as in the quiz_attempts.state DB column.
* @param int|null $relativetimefinish time relative to now when the attempt finished, or null for 0.
* @param int|null $relativetimeclose time relative to now when the quiz closes, or null for 0.
* @param int $expectedstate expected result. One of the display_options constants.
* @covers ::quiz_attempt_state
*/
public function test_quiz_attempt_state(string $attemptstate,
?int $relativetimefinish, ?int $relativetimeclose, int $expectedstate): void {
$attempt = new \stdClass();
$attempt->state = $attemptstate;
if ($relativetimefinish === null) {
$attempt->timefinish = 0;
} else {
$attempt->timefinish = time() + $relativetimefinish;
}
$quiz = new \stdClass();
if ($relativetimeclose === null) {
$quiz->timeclose = 0;
} else {
$quiz->timeclose = time() + $relativetimeclose;
}
$this->assertEquals($expectedstate, quiz_attempt_state($quiz, $attempt));
}
/**
* @covers ::quiz_question_tostring
*/
public function test_quiz_question_tostring(): void {
$question = new \stdClass();
$question->qtype = 'multichoice';
$question->name = 'The question name';
$question->questiontext = '<p>What sort of <b>inequality</b> is x &lt; y<img alt="?" src="..."></p>';
$question->questiontextformat = FORMAT_HTML;
$summary = quiz_question_tostring($question);
$this->assertEquals('<span class="questionname">The question name</span> ' .
'<span class="questiontext">What sort of INEQUALITY is x &lt; y[?]' . "\n" . '</span>', $summary);
}
/**
* @covers ::quiz_question_tostring
*/
public function test_quiz_question_tostring_does_not_filter(): void {
$question = new \stdClass();
$question->qtype = 'multichoice';
$question->name = 'The question name';
$question->questiontext = '<p>No emoticons here :-)</p>';
$question->questiontextformat = FORMAT_HTML;
$summary = quiz_question_tostring($question);
$this->assertEquals('<span class="questionname">The question name</span> ' .
'<span class="questiontext">No emoticons here :-)' . "\n</span>", $summary);
}
/**
* Test quiz_view
* @return void
*/
public function test_quiz_view(): void {
global $CFG;
$CFG->enablecompletion = 1;
$this->resetAfterTest();
$this->setAdminUser();
// Setup test data.
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id],
['completion' => 2, 'completionview' => 1]);
$context = \context_module::instance($quiz->cmid);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
// Trigger and capture the event.
$sink = $this->redirectEvents();
quiz_view($quiz, $course, $cm, $context);
$events = $sink->get_events();
// 2 additional events thanks to completion.
$this->assertCount(3, $events);
$event = array_shift($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\mod_quiz\event\course_module_viewed', $event);
$this->assertEquals($context, $event->get_context());
$moodleurl = new \moodle_url('/mod/quiz/view.php', ['id' => $cm->id]);
$this->assertEquals($moodleurl, $event->get_url());
$this->assertEventContextNotUsed($event);
$this->assertNotEmpty($event->get_name());
// Check completion status.
$completion = new \completion_info($course);
$completiondata = $completion->get_data($cm);
$this->assertEquals(1, $completiondata->completionstate);
}
/**
* Return false when there are not overrides for this quiz instance.
*/
public function test_quiz_is_overriden_calendar_event_no_override(): void {
global $CFG, $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id]);
$event = new \calendar_event((object)[
'modulename' => 'quiz',
'instance' => $quiz->id,
'userid' => $user->id
]);
$this->assertFalse(quiz_is_overriden_calendar_event($event));
}
/**
* Return false if the given event isn't an quiz module event.
*/
public function test_quiz_is_overriden_calendar_event_no_module_event(): void {
global $CFG, $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id]);
$event = new \calendar_event((object)[
'userid' => $user->id
]);
$this->assertFalse(quiz_is_overriden_calendar_event($event));
}
/**
* Return false if there is overrides for this use but they belong to another quiz
* instance.
*/
public function test_quiz_is_overriden_calendar_event_different_quiz_instance(): void {
global $CFG, $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id]);
$quiz2 = $quizgenerator->create_instance(['course' => $course->id]);
$event = new \calendar_event((object) [
'modulename' => 'quiz',
'instance' => $quiz->id,
'userid' => $user->id
]);
$record = (object) [
'quiz' => $quiz2->id,
'userid' => $user->id
];
$DB->insert_record('quiz_overrides', $record);
$this->assertFalse(quiz_is_overriden_calendar_event($event));
}
/**
* Return true if there is a user override for this event and quiz instance.
*/
public function test_quiz_is_overriden_calendar_event_user_override(): void {
global $CFG, $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id]);
$event = new \calendar_event((object) [
'modulename' => 'quiz',
'instance' => $quiz->id,
'userid' => $user->id
]);
$record = (object) [
'quiz' => $quiz->id,
'userid' => $user->id
];
$DB->insert_record('quiz_overrides', $record);
$this->assertTrue(quiz_is_overriden_calendar_event($event));
}
/**
* Return true if there is a group override for the event and quiz instance.
*/
public function test_quiz_is_overriden_calendar_event_group_override(): void {
global $CFG, $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id]);
$group = $this->getDataGenerator()->create_group(['courseid' => $quiz->course]);
$groupid = $group->id;
$userid = $user->id;
$event = new \calendar_event((object) [
'modulename' => 'quiz',
'instance' => $quiz->id,
'groupid' => $groupid
]);
$record = (object) [
'quiz' => $quiz->id,
'groupid' => $groupid
];
$DB->insert_record('quiz_overrides', $record);
$this->assertTrue(quiz_is_overriden_calendar_event($event));
}
/**
* Test test_quiz_get_user_timeclose().
*/
public function test_quiz_get_user_timeclose(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$basetimestamp = time(); // The timestamp we will base the enddates on.
// Create generator, course and quizzes.
$student1 = $this->getDataGenerator()->create_user();
$student2 = $this->getDataGenerator()->create_user();
$student3 = $this->getDataGenerator()->create_user();
$teacher = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
// Both quizzes close in two hours.
$quiz1 = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => $basetimestamp + 7200]);
$quiz2 = $quizgenerator->create_instance(['course' => $course->id, 'timeclose' => $basetimestamp + 7200]);
$group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$student1id = $student1->id;
$student2id = $student2->id;
$student3id = $student3->id;
$teacherid = $teacher->id;
// Users enrolments.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
$this->getDataGenerator()->enrol_user($student1id, $course->id, $studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($student2id, $course->id, $studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($student3id, $course->id, $studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($teacherid, $course->id, $teacherrole->id, 'manual');
// Create groups.
$group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$group1id = $group1->id;
$group2id = $group2->id;
$this->getDataGenerator()->create_group_member(['userid' => $student1id, 'groupid' => $group1id]);
$this->getDataGenerator()->create_group_member(['userid' => $student2id, 'groupid' => $group2id]);
// Group 1 gets an group override for quiz 1 to close in three hours.
$record1 = (object) [
'quiz' => $quiz1->id,
'groupid' => $group1id,
'timeclose' => $basetimestamp + 10800 // In three hours.
];
$DB->insert_record('quiz_overrides', $record1);
// Let's test quiz 1 closes in three hours for user student 1 since member of group 1.
// Quiz 2 closes in two hours.
$this->setUser($student1id);
$params = new \stdClass();
$comparearray = [];
$object = new \stdClass();
$object->id = $quiz1->id;
$object->usertimeclose = $basetimestamp + 10800; // The overriden timeclose for quiz 1.
$comparearray[$quiz1->id] = $object;
$object = new \stdClass();
$object->id = $quiz2->id;
$object->usertimeclose = $basetimestamp + 7200; // The unchanged timeclose for quiz 2.
$comparearray[$quiz2->id] = $object;
$this->assertEquals($comparearray, quiz_get_user_timeclose($course->id));
// Let's test quiz 1 closes in two hours (the original value) for user student 3 since member of no group.
$this->setUser($student3id);
$params = new \stdClass();
$comparearray = [];
$object = new \stdClass();
$object->id = $quiz1->id;
$object->usertimeclose = $basetimestamp + 7200; // The original timeclose for quiz 1.
$comparearray[$quiz1->id] = $object;
$object = new \stdClass();
$object->id = $quiz2->id;
$object->usertimeclose = $basetimestamp + 7200; // The original timeclose for quiz 2.
$comparearray[$quiz2->id] = $object;
$this->assertEquals($comparearray, quiz_get_user_timeclose($course->id));
// User 2 gets an user override for quiz 1 to close in four hours.
$record2 = (object) [
'quiz' => $quiz1->id,
'userid' => $student2id,
'timeclose' => $basetimestamp + 14400 // In four hours.
];
$DB->insert_record('quiz_overrides', $record2);
// Let's test quiz 1 closes in four hours for user student 2 since personally overriden.
// Quiz 2 closes in two hours.
$this->setUser($student2id);
$comparearray = [];
$object = new \stdClass();
$object->id = $quiz1->id;
$object->usertimeclose = $basetimestamp + 14400; // The overriden timeclose for quiz 1.
$comparearray[$quiz1->id] = $object;
$object = new \stdClass();
$object->id = $quiz2->id;
$object->usertimeclose = $basetimestamp + 7200; // The unchanged timeclose for quiz 2.
$comparearray[$quiz2->id] = $object;
$this->assertEquals($comparearray, quiz_get_user_timeclose($course->id));
// Let's test a teacher sees the original times.
// Quiz 1 and quiz 2 close in two hours.
$this->setUser($teacherid);
$comparearray = [];
$object = new \stdClass();
$object->id = $quiz1->id;
$object->usertimeclose = $basetimestamp + 7200; // The unchanged timeclose for quiz 1.
$comparearray[$quiz1->id] = $object;
$object = new \stdClass();
$object->id = $quiz2->id;
$object->usertimeclose = $basetimestamp + 7200; // The unchanged timeclose for quiz 2.
$comparearray[$quiz2->id] = $object;
$this->assertEquals($comparearray, quiz_get_user_timeclose($course->id));
}
/**
* This function creates a quiz with some standard (non-random) and some random questions.
* The standard questions are created first and then random questions follow them.
* So in a quiz with 3 standard question and 2 random question, the first random question is at slot 4.
*
* @param int $qnum Number of standard questions that should be created in the quiz.
* @param int $randomqnum Number of random questions that should be created in the quiz.
* @param array $questiontags Tags to be used for random questions.
* This is an array in the following format:
* [
* 0 => ['foo', 'bar'],
* 1 => ['baz', 'qux']
* ]
* @param string[] $unusedtags Some additional tags to be created.
* @return array An array of 2 elements: $quiz and $tagobjects.
* $tagobjects is an associative array of all created tag objects with its key being tag names.
*/
private function setup_quiz_and_tags($qnum, $randomqnum, $questiontags = [], $unusedtags = []) {
global $SITE;
$tagobjects = [];
// Get all the tags that need to be created.
$alltags = [];
foreach ($questiontags as $questiontag) {
$alltags = array_merge($alltags, $questiontag);
}
$alltags = array_merge($alltags, $unusedtags);
$alltags = array_unique($alltags);
// Create tags.
foreach ($alltags as $tagname) {
$tagrecord = [
'isstandard' => 1,
'flag' => 0,
'rawname' => $tagname,
'description' => $tagname . ' desc'
];
$tagobjects[$tagname] = $this->getDataGenerator()->create_tag($tagrecord);
}
// Create a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0]);
// Create a question category in the system context.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
// Setup standard questions.
for ($i = 0; $i < $qnum; $i++) {
$question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
}
// Setup random questions.
for ($i = 0; $i < $randomqnum; $i++) {
// Just create a standard question first, so there would be enough questions to pick a random question from.
$question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$tagids = [];
if (!empty($questiontags[$i])) {
foreach ($questiontags[$i] as $tagname) {
$tagids[] = $tagobjects[$tagname]->id;
}
}
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
}
return [$quiz, $tagobjects];
}
public function test_quiz_override_summary(): void {
global $DB, $PAGE;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
/** @var mod_quiz_generator $quizgenerator */
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
/** @var renderer $renderer */
$renderer = $PAGE->get_renderer('mod_quiz');
// Course with quiz and a group - plus some others, to verify they don't get counted.
$course = $generator->create_course();
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'groupmode' => SEPARATEGROUPS]);
$cm = get_coursemodule_from_id('quiz', $quiz->cmid, $course->id);
$group = $generator->create_group(['courseid' => $course->id]);
$othergroup = $generator->create_group(['courseid' => $course->id]);
$otherquiz = $quizgenerator->create_instance(['course' => $course->id]);
// Initial test (as admin) with no data.
$this->setAdminUser();
$this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'allgroups'],
quiz_override_summary($quiz, $cm));
$this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'onegroup'],
quiz_override_summary($quiz, $cm, $group->id));
// Editing teacher.
$teacher = $generator->create_user();
$generator->enrol_user($teacher->id, $course->id, 'editingteacher');
// Non-editing teacher.
$tutor = $generator->create_user();
$generator->enrol_user($tutor->id, $course->id, 'teacher');
$generator->create_group_member(['userid' => $tutor->id, 'groupid' => $group->id]);
// Three students.
$student1 = $generator->create_user();
$generator->enrol_user($student1->id, $course->id, 'student');
$generator->create_group_member(['userid' => $student1->id, 'groupid' => $group->id]);
$student2 = $generator->create_user();
$generator->enrol_user($student2->id, $course->id, 'student');
$generator->create_group_member(['userid' => $student2->id, 'groupid' => $othergroup->id]);
$student3 = $generator->create_user();
$generator->enrol_user($student3->id, $course->id, 'student');
// Initial test now users exist, but before overrides.
// Test as teacher.
$this->setUser($teacher);
$this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'allgroups'],
quiz_override_summary($quiz, $cm));
$this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'onegroup'],
quiz_override_summary($quiz, $cm, $group->id));
// Test as tutor.
$this->setUser($tutor);
$this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'somegroups'],
quiz_override_summary($quiz, $cm));
$this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'onegroup'],
quiz_override_summary($quiz, $cm, $group->id));
$this->assertEquals('', $renderer->quiz_override_summary_links($quiz, $cm));
// Quiz setting overrides for students 1 and 3.
$quizgenerator->create_override(['quiz' => $quiz->id, 'userid' => $student1->id, 'attempts' => 2]);
$quizgenerator->create_override(['quiz' => $quiz->id, 'userid' => $student3->id, 'attempts' => 2]);
$quizgenerator->create_override(['quiz' => $quiz->id, 'groupid' => $group->id, 'attempts' => 3]);
$quizgenerator->create_override(['quiz' => $quiz->id, 'groupid' => $othergroup->id, 'attempts' => 3]);
$quizgenerator->create_override(['quiz' => $otherquiz->id, 'userid' => $student2->id, 'attempts' => 2]);
// Test as teacher.
$this->setUser($teacher);
$this->assertEquals(['group' => 2, 'user' => 2, 'mode' => 'allgroups'],
quiz_override_summary($quiz, $cm));
$this->assertEquals('Settings overrides exist (Groups: 2, Users: 2)',
// Links checked by Behat, so strip them for these tests.
html_to_text($renderer->quiz_override_summary_links($quiz, $cm), 0, false));
$this->assertEquals(['group' => 1, 'user' => 1, 'mode' => 'onegroup'],
quiz_override_summary($quiz, $cm, $group->id));
$this->assertEquals('Settings overrides exist (Groups: 1, Users: 1) for this group',
html_to_text($renderer->quiz_override_summary_links($quiz, $cm, $group->id), 0, false));
// Test as tutor.
$this->setUser($tutor);
$this->assertEquals(['group' => 1, 'user' => 1, 'mode' => 'somegroups'],
quiz_override_summary($quiz, $cm));
$this->assertEquals('Settings overrides exist (Groups: 1, Users: 1) for your groups',
html_to_text($renderer->quiz_override_summary_links($quiz, $cm), 0, false));
$this->assertEquals(['group' => 1, 'user' => 1, 'mode' => 'onegroup'],
quiz_override_summary($quiz, $cm, $group->id));
$this->assertEquals('Settings overrides exist (Groups: 1, Users: 1) for this group',
html_to_text($renderer->quiz_override_summary_links($quiz, $cm, $group->id), 0, false));
// Now set the quiz to be group mode: no groups, and re-test as tutor.
// In this case, the tutor should see all groups.
$DB->set_field('course_modules', 'groupmode', NOGROUPS, ['id' => $cm->id]);
$cm = get_coursemodule_from_id('quiz', $quiz->cmid, $course->id);
$this->assertEquals(['group' => 2, 'user' => 2, 'mode' => 'allgroups'],
quiz_override_summary($quiz, $cm));
$this->assertEquals('Settings overrides exist (Groups: 2, Users: 2)',
html_to_text($renderer->quiz_override_summary_links($quiz, $cm), 0, false));
}
/**
* Test quiz_send_confirmation function.
*/
public function test_quiz_send_confirmation(): void {
global $CFG, $DB;
$this->resetAfterTest();
$this->setAdminUser();
$this->preventResetByRollback();
$course = $this->getDataGenerator()->create_course();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id]);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
$recipient = $this->getDataGenerator()->create_user(['email' => 'student@example.com']);
// Allow recipent to receive email confirm submission.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
assign_capability('mod/quiz:emailconfirmsubmission', CAP_ALLOW, $studentrole->id,
\context_course::instance($course->id), true);
$this->getDataGenerator()->enrol_user($recipient->id, $course->id, $studentrole->id, 'manual');
$timenow = time();
$data = new \stdClass();
// Course info.
$data->courseid = $course->id;
$data->coursename = $course->fullname;
// Quiz info.
$data->quizname = $quiz->name;
$data->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
$data->quizid = $quiz->id;
$data->quizcmid = $quiz->cmid;
$data->attemptid = 1;
$data->submissiontime = userdate($timenow);
$sink = $this->redirectEmails();
quiz_send_confirmation($recipient, $data, true);
$messages = $sink->get_messages();
$message = reset($messages);
$this->assertStringContainsString("Thank you for submitting your answers" ,
quoted_printable_decode($message->body));
$sink->close();
$sink = $this->redirectEmails();
quiz_send_confirmation($recipient, $data, false);
$messages = $sink->get_messages();
$message = reset($messages);
$this->assertStringContainsString("Your answers were submitted automatically" ,
quoted_printable_decode($message->body));
$sink->close();
}
}
@@ -0,0 +1,206 @@
<?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\output;
use advanced_testcase;
/**
* Tests for {@see attempt_summary_information}.
*
* @package mod_quiz
* @copyright The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\output\attempt_summary_information
*/
final class attempt_summary_information_test extends advanced_testcase {
public function test_add_item(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item('test', 'Test name', 'Test value');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals([(object) ['title' => 'Test name', 'content' => 'Test value']], $data['items']);
}
public function test_add_item_before_start(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item('test', 'Test name', 'Test value');
$summary->add_item_before('newitem', 'New name', 'New value', 'test');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals(
[
(object) ['title' => 'New name', 'content' => 'New value'],
(object) ['title' => 'Test name', 'content' => 'Test value'],
],
$data['items'],
);
}
public function test_add_item_before_middle(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item('item1', 'Existing 1', 'One');
$summary->add_item('item2', 'Existing 2', 'Two');
$summary->add_item_before('newitem', 'New name', 'New value', 'item2');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals(
[
(object) ['title' => 'Existing 1', 'content' => 'One'],
(object) ['title' => 'New name', 'content' => 'New value'],
(object) ['title' => 'Existing 2', 'content' => 'Two'],
],
$data['items'],
);
}
public function test_add_item_before_no_match(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item('test', 'Test name', 'Test value');
$summary->add_item_before('newitem', 'New name', 'New value', 'unknown');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals(
[
(object) ['title' => 'New name', 'content' => 'New value'],
(object) ['title' => 'Test name', 'content' => 'Test value'],
],
$data['items'],
);
}
public function test_add_item_before_empty(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item_before('newitem', 'New name', 'New value', 'unknown');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals([(object) ['title' => 'New name', 'content' => 'New value']], $data['items']);
}
public function test_add_item_after_end(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item('test', 'Test name', 'Test value');
$summary->add_item_after('newitem', 'New name', 'New value', 'test');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals(
[
(object) ['title' => 'Test name', 'content' => 'Test value'],
(object) ['title' => 'New name', 'content' => 'New value'],
],
$data['items'],
);
}
public function test_add_item_after_middle(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item('item1', 'Existing 1', 'One');
$summary->add_item('item2', 'Existing 2', 'Two');
$summary->add_item_after('newitem', 'New name', 'New value', 'item1');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals(
[
(object) ['title' => 'Existing 1', 'content' => 'One'],
(object) ['title' => 'New name', 'content' => 'New value'],
(object) ['title' => 'Existing 2', 'content' => 'Two'],
],
$data['items'],
);
}
public function test_add_item_after_no_match(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item('test', 'Test name', 'Test value');
$summary->add_item_after('newitem', 'New name', 'New value', 'unknown');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals(
[
(object) ['title' => 'Test name', 'content' => 'Test value'],
(object) ['title' => 'New name', 'content' => 'New value'],
],
$data['items'],
);
}
public function test_add_item_after_empty(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item_after('newitem', 'New name', 'New value', 'unknown');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals([(object) ['title' => 'New name', 'content' => 'New value']], $data['items']);
}
public function test_remove_item(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item('item1', 'Existing 1', 'One');
$summary->add_item('item2', 'Existing 2', 'Two');
$summary->remove_item('item1');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals([(object) ['title' => 'Existing 2', 'content' => 'Two']], $data['items']);
}
public function test_remove_item_not_present(): void {
global $PAGE;
$summary = new attempt_summary_information();
$summary->add_item('item1', 'Existing 1', 'One');
$summary->add_item('item2', 'Existing 2', 'Two');
$summary->remove_item('item3');
$data = $summary->export_for_template($PAGE->get_renderer('mod_quiz'));
$this->assertEquals(
[
(object) ['title' => 'Existing 1', 'content' => 'One'],
(object) ['title' => 'Existing 2', 'content' => 'Two'],
],
$data['items'],
);
}
}
+559
View File
@@ -0,0 +1,559 @@
<?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/>.
/**
* Privacy provider tests.
*
* @package mod_quiz
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\deletion_criteria;
use core_privacy\local\request\writer;
use mod_quiz\privacy\provider;
use mod_quiz\privacy\helper;
use mod_quiz\quiz_attempt;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/tests/privacy_helper.php');
/**
* Privacy provider tests class.
*
* @package mod_quiz
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\privacy\provider
*/
class provider_test extends \core_privacy\tests\provider_testcase {
use \core_question_privacy_helper;
/**
* Test that a user who has no data gets no contexts
*/
public function test_get_contexts_for_userid_no_data(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$contextlist = provider::get_contexts_for_userid($USER->id);
$this->assertEmpty($contextlist);
}
/**
* Test for provider::get_contexts_for_userid() when there is no quiz attempt at all.
*/
public function test_get_contexts_for_userid_no_attempt_with_override(): void {
global $DB;
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
// Make a quiz with an override.
$this->setUser();
$quiz = $this->create_test_quiz($course);
$DB->insert_record('quiz_overrides', [
'quiz' => $quiz->id,
'userid' => $user->id,
'timeclose' => 1300,
'timelimit' => null,
]);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
$context = \context_module::instance($cm->id);
// Fetch the contexts - only one context should be returned.
$this->setUser();
$contextlist = provider::get_contexts_for_userid($user->id);
$this->assertCount(1, $contextlist);
$this->assertEquals($context, $contextlist->current());
}
/**
* The export function should handle an empty contextlist properly.
*/
public function test_export_user_data_no_data(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
\core_user::get_user($USER->id),
'mod_quiz',
[]
);
provider::export_user_data($approvedcontextlist);
$this->assertDebuggingNotCalled();
// No data should have been exported.
$writer = \core_privacy\local\request\writer::with_context(\context_system::instance());
$this->assertFalse($writer->has_any_data_in_any_context());
}
/**
* The delete function should handle an empty contextlist properly.
*/
public function test_delete_data_for_user_no_data(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
\core_user::get_user($USER->id),
'mod_quiz',
[]
);
provider::delete_data_for_user($approvedcontextlist);
$this->assertDebuggingNotCalled();
}
/**
* Export + Delete quiz data for a user who has made a single attempt.
*/
public function test_user_with_data(): void {
global $DB;
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$otheruser = $this->getDataGenerator()->create_user();
// Make a quiz with an override.
$this->setUser();
$quiz = $this->create_test_quiz($course);
$DB->insert_record('quiz_overrides', [
'quiz' => $quiz->id,
'userid' => $user->id,
'timeclose' => 1300,
'timelimit' => null,
]);
// Run as the user and make an attempt on the quiz.
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user);
$this->attempt_quiz($quiz, $otheruser);
$context = $quizobj->get_context();
// Fetch the contexts - only one context should be returned.
$this->setUser();
$contextlist = provider::get_contexts_for_userid($user->id);
$this->assertCount(1, $contextlist);
$this->assertEquals($context, $contextlist->current());
// Perform the export and check the data.
$this->setUser($user);
$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
\core_user::get_user($user->id),
'mod_quiz',
$contextlist->get_contextids()
);
provider::export_user_data($approvedcontextlist);
// Ensure that the quiz data was exported correctly.
/** @var \core_privacy\tests\request\content_writer $writer */
$writer = writer::with_context($context);
$this->assertTrue($writer->has_any_data());
$quizdata = $writer->get_data([]);
$this->assertEquals($quizobj->get_quiz_name(), $quizdata->name);
// Every module has an intro.
$this->assertTrue(isset($quizdata->intro));
// Fetch the attempt data.
$attempt = $attemptobj->get_attempt();
$attemptsubcontext = [
get_string('attempts', 'mod_quiz'),
$attempt->attempt,
];
$attemptdata = writer::with_context($context)->get_data($attemptsubcontext);
$attempt = $attemptobj->get_attempt();
$this->assertTrue(isset($attemptdata->state));
$this->assertEquals(quiz_attempt::state_name($attemptobj->get_state()), $attemptdata->state);
$this->assertTrue(isset($attemptdata->timestart));
$this->assertTrue(isset($attemptdata->timefinish));
$this->assertTrue(isset($attemptdata->timemodified));
$this->assertFalse(isset($attemptdata->timemodifiedoffline));
$this->assertFalse(isset($attemptdata->timecheckstate));
$this->assertTrue(isset($attemptdata->grade));
$this->assertEquals(100.00, $attemptdata->grade->grade);
// Check that the exported question attempts are correct.
$attemptsubcontext = helper::get_quiz_attempt_subcontext($attemptobj->get_attempt(), $user);
$this->assert_question_attempt_exported(
$context,
$attemptsubcontext,
\question_engine::load_questions_usage_by_activity($attemptobj->get_uniqueid()),
quiz_get_review_options($quiz, $attemptobj->get_attempt(), $context),
$user
);
// Delete the data and check it is removed.
$this->setUser();
provider::delete_data_for_user($approvedcontextlist);
$this->expectException(\dml_missing_record_exception::class);
quiz_attempt::create($attemptobj->get_quizid());
}
/**
* Export + Delete quiz data for a user who has made a single attempt.
*/
public function test_user_with_preview(): void {
global $DB;
$this->resetAfterTest(true);
// Make a quiz.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance([
'course' => $course->id,
'questionsperpage' => 0,
'grade' => 100.0,
'sumgrades' => 2,
]);
// Create a couple of questions.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($saq->id, $quiz);
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
quiz_add_quiz_question($numq->id, $quiz);
// Run as the user and make an attempt on the quiz.
$this->setUser($user);
$starttime = time();
$quizobj = \mod_quiz\quiz_settings::create($quiz->id, $user->id);
$context = $quizobj->get_context();
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
// Start the attempt.
$attempt = quiz_create_attempt($quizobj, 1, false, $starttime, true, $user->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Answer the questions.
$attemptobj = quiz_attempt::create($attempt->id);
$tosubmit = [
1 => ['answer' => 'frog'],
2 => ['answer' => '3.14'],
];
$attemptobj->process_submitted_actions($starttime, false, $tosubmit);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
$attemptobj->process_finish($starttime, false);
// Fetch the contexts - no context should be returned.
$this->setUser();
$contextlist = provider::get_contexts_for_userid($user->id);
$this->assertCount(0, $contextlist);
}
/**
* Export + Delete quiz data for a user who has made a single attempt.
*/
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$otheruser = $this->getDataGenerator()->create_user();
// Make a quiz with an override.
$this->setUser();
$quiz = $this->create_test_quiz($course);
$DB->insert_record('quiz_overrides', [
'quiz' => $quiz->id,
'userid' => $user->id,
'timeclose' => 1300,
'timelimit' => null,
]);
// Run as the user and make an attempt on the quiz.
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user);
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $otheruser);
// Create another quiz and questions, and repeat the data insertion.
$this->setUser();
$otherquiz = $this->create_test_quiz($course);
$DB->insert_record('quiz_overrides', [
'quiz' => $otherquiz->id,
'userid' => $user->id,
'timeclose' => 1300,
'timelimit' => null,
]);
// Run as the user and make an attempt on the quiz.
list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $user);
list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $otheruser);
// Delete all data for all users in the context under test.
$this->setUser();
$context = $quizobj->get_context();
provider::delete_data_for_all_users_in_context($context);
// The quiz attempt should have been deleted from this quiz.
$this->assertCount(0, $DB->get_records('quiz_attempts', ['quiz' => $quizobj->get_quizid()]));
$this->assertCount(0, $DB->get_records('quiz_overrides', ['quiz' => $quizobj->get_quizid()]));
$this->assertCount(0, $DB->get_records('question_attempts', ['questionusageid' => $quba->get_id()]));
// But not for the other quiz.
$this->assertNotCount(0, $DB->get_records('quiz_attempts', ['quiz' => $otherquizobj->get_quizid()]));
$this->assertNotCount(0, $DB->get_records('quiz_overrides', ['quiz' => $otherquizobj->get_quizid()]));
$this->assertNotCount(0, $DB->get_records('question_attempts', ['questionusageid' => $otherquba->get_id()]));
}
/**
* Export + Delete quiz data for a user who has made a single attempt.
*/
public function test_wrong_context(): void {
global $DB;
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
// Make a choice.
$this->setUser();
$plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
$choice = $plugingenerator->create_instance(['course' => $course->id]);
$cm = get_coursemodule_from_instance('choice', $choice->id);
$context = \context_module::instance($cm->id);
// Fetch the contexts - no context should be returned.
$this->setUser();
$contextlist = provider::get_contexts_for_userid($user->id);
$this->assertCount(0, $contextlist);
// Perform the export and check the data.
$this->setUser($user);
$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
\core_user::get_user($user->id),
'mod_quiz',
[$context->id]
);
provider::export_user_data($approvedcontextlist);
// Ensure that nothing was exported.
/** @var \core_privacy\tests\request\content_writer $writer */
$writer = writer::with_context($context);
$this->assertFalse($writer->has_any_data_in_any_context());
$this->setUser();
$dbwrites = $DB->perf_get_writes();
// Perform a deletion with the approved contextlist containing an incorrect context.
$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
\core_user::get_user($user->id),
'mod_quiz',
[$context->id]
);
provider::delete_data_for_user($approvedcontextlist);
$this->assertEquals($dbwrites, $DB->perf_get_writes());
$this->assertDebuggingNotCalled();
// Perform a deletion of all data in the context.
provider::delete_data_for_all_users_in_context($context);
$this->assertEquals($dbwrites, $DB->perf_get_writes());
$this->assertDebuggingNotCalled();
}
/**
* Create a test quiz for the specified course.
*
* @param \stdClass $course
* @return \stdClass
*/
protected function create_test_quiz($course) {
global $DB;
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance([
'course' => $course->id,
'questionsperpage' => 0,
'grade' => 100.0,
'sumgrades' => 2,
]);
// Create a couple of questions.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($saq->id, $quiz);
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
quiz_add_quiz_question($numq->id, $quiz);
return $quiz;
}
/**
* Answer questions for a quiz + user.
*
* @param \stdClass $quiz
* @param \stdClass $user
* @return array
*/
protected function attempt_quiz($quiz, $user) {
$this->setUser($user);
$starttime = time();
$quizobj = \mod_quiz\quiz_settings::create($quiz->id, $user->id);
$context = $quizobj->get_context();
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
// Start the attempt.
$attempt = quiz_create_attempt($quizobj, 1, false, $starttime, false, $user->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Answer the questions.
$attemptobj = quiz_attempt::create($attempt->id);
$tosubmit = [
1 => ['answer' => 'frog'],
2 => ['answer' => '3.14'],
];
$attemptobj->process_submitted_actions($starttime, false, $tosubmit);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_finish($starttime, false);
$this->setUser();
return [$quizobj, $quba, $attemptobj];
}
/**
* Test for provider::get_users_in_context().
*/
public function test_get_users_in_context(): void {
global $DB;
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$anotheruser = $this->getDataGenerator()->create_user();
$extrauser = $this->getDataGenerator()->create_user();
// Make a quiz.
$this->setUser();
$quiz = $this->create_test_quiz($course);
// Create an override for user1.
$DB->insert_record('quiz_overrides', [
'quiz' => $quiz->id,
'userid' => $user->id,
'timeclose' => 1300,
'timelimit' => null,
]);
// Make an attempt on the quiz as user2.
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $anotheruser);
$context = $quizobj->get_context();
// Fetch users - user1 and user2 should be returned.
$userlist = new \core_privacy\local\request\userlist($context, 'mod_quiz');
provider::get_users_in_context($userlist);
$this->assertEqualsCanonicalizing(
[$user->id, $anotheruser->id],
$userlist->get_userids());
}
/**
* Test for provider::delete_data_for_users().
*/
public function test_delete_data_for_users(): void {
global $DB;
$this->resetAfterTest(true);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
// Make a quiz in each course.
$quiz1 = $this->create_test_quiz($course1);
$quiz2 = $this->create_test_quiz($course2);
// Attempt quiz1 as user1 and user2.
list($quiz1obj) = $this->attempt_quiz($quiz1, $user1);
$this->attempt_quiz($quiz1, $user2);
// Create an override in quiz1 for user3.
$DB->insert_record('quiz_overrides', [
'quiz' => $quiz1->id,
'userid' => $user3->id,
'timeclose' => 1300,
'timelimit' => null,
]);
// Attempt quiz2 as user1.
$this->attempt_quiz($quiz2, $user1);
// Delete the data for user1 and user3 in course1 and check it is removed.
$quiz1context = $quiz1obj->get_context();
$approveduserlist = new \core_privacy\local\request\approved_userlist($quiz1context, 'mod_quiz',
[$user1->id, $user3->id]);
provider::delete_data_for_users($approveduserlist);
// Only the attempt of user2 should be remained in quiz1.
$this->assertEquals(
[$user2->id],
$DB->get_fieldset_select('quiz_attempts', 'userid', 'quiz = ?', [$quiz1->id])
);
// The attempt that user1 made in quiz2 should be remained.
$this->assertEquals(
[$user1->id],
$DB->get_fieldset_select('quiz_attempts', 'userid', 'quiz = ?', [$quiz2->id])
);
// The quiz override in quiz1 that we had for user3 should be deleted.
$this->assertEquals(
[],
$DB->get_fieldset_select('quiz_overrides', 'userid', 'quiz = ?', [$quiz1->id])
);
}
}
@@ -0,0 +1,186 @@
<?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/>.
/**
* Unit tests for the privacy legacy polyfill for quiz access rules.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz;
/**
* Unit tests for the privacy legacy polyfill for quiz access rules.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class privacy_legacy_quizaccess_polyfill_test extends \advanced_testcase {
/**
* Test that the core_quizaccess\privacy\legacy_polyfill works and that the static _export_quizaccess_user_data can
* be called.
*/
public function test_export_quizaccess_user_data(): void {
$quiz = $this->createMock(quiz_settings::class);
$user = (object) [];
$returnvalue = (object) [];
$mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class);
$mock->expects($this->once())
->method('get_return_value')
->with('_export_quizaccess_user_data', [$quiz, $user])
->willReturn($returnvalue);
test_privacy_legacy_quizaccess_polyfill_provider::$mock = $mock;
$result = test_privacy_legacy_quizaccess_polyfill_provider::export_quizaccess_user_data($quiz, $user);
$this->assertSame($returnvalue, $result);
}
/**
* Test the _delete_quizaccess_for_context shim.
*/
public function test_delete_quizaccess_for_context(): void {
$context = \context_system::instance();
$quiz = $this->createMock(quiz_settings::class);
$mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class);
$mock->expects($this->once())
->method('get_return_value')
->with('_delete_quizaccess_data_for_all_users_in_context', [$quiz]);
test_privacy_legacy_quizaccess_polyfill_provider::$mock = $mock;
test_privacy_legacy_quizaccess_polyfill_provider::delete_quizaccess_data_for_all_users_in_context($quiz);
}
/**
* Test the _delete_quizaccess_for_user shim.
*/
public function test_delete_quizaccess_for_user(): void {
$context = \context_system::instance();
$quiz = $this->createMock(quiz_settings::class);
$user = (object) [];
$mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class);
$mock->expects($this->once())
->method('get_return_value')
->with('_delete_quizaccess_data_for_user', [$quiz, $user]);
test_privacy_legacy_quizaccess_polyfill_provider::$mock = $mock;
test_privacy_legacy_quizaccess_polyfill_provider::delete_quizaccess_data_for_user($quiz, $user);
}
/**
* Test the _delete_quizaccess_for_users shim.
*/
public function test_delete_quizaccess_for_users(): void {
$context = $this->createMock(\context_module::class);
$user = (object) [];
$approveduserlist = new \core_privacy\local\request\approved_userlist($context, 'mod_quiz', [$user]);
$mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class);
$mock->expects($this->once())
->method('get_return_value')
->with('_delete_quizaccess_data_for_users', [$approveduserlist]);
test_privacy_legacy_quizaccess_polyfill_provider::$mock = $mock;
test_privacy_legacy_quizaccess_polyfill_provider::delete_quizaccess_data_for_users($approveduserlist);
}
}
/**
* Legacy polyfill test class for the quizaccess_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_privacy_legacy_quizaccess_polyfill_provider implements
\core_privacy\local\metadata\provider,
\mod_quiz\privacy\quizaccess_provider,
\mod_quiz\privacy\quizaccess_user_provider {
use \mod_quiz\privacy\legacy_quizaccess_polyfill;
use \core_privacy\local\legacy_polyfill;
/**
* @var test_privacy_legacy_quizaccess_polyfill_provider $mock.
*/
public static $mock = null;
/**
* Export all user data for the quizaccess plugin.
*
* @param \mod_quiz\quiz_settings $quiz
* @param \stdClass $user
*/
protected static function _export_quizaccess_user_data($quiz, $user) {
return static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* Deletes all user data for the given context.
*
* @param \mod_quiz\quiz_settings $quiz
*/
protected static function _delete_quizaccess_data_for_all_users_in_context($quiz) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* Delete personal data for the given user and context.
*
* @param \mod_quiz\quiz_settings $quiz The quiz being deleted
* @param \stdClass $user The user to export data for
*/
protected static function _delete_quizaccess_data_for_user($quiz, $user) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* Delete all user data for the specified users, in the specified context.
*
* @param \core_privacy\local\request\approved_userlist $userlist
*/
protected static function _delete_quizaccess_data_for_users($userlist) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* Returns metadata about this plugin.
*
* @param \core_privacy\local\metadata\collection $collection The initialised collection to add items to.
* @return \core_privacy\local\metadata\collection A listing of user data stored through this system.
*/
protected static function _get_metadata(\core_privacy\local\metadata\collection $collection) {
return $collection;
}
}
/**
* Called inside the polyfill methods in the test polyfill provider, allowing us to ensure these are called with correct params.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_privacy_legacy_quizaccess_polyfill_mock_wrapper {
/**
* Get the return value for the specified item.
*/
public function get_return_value() {
}
}
+206
View File
@@ -0,0 +1,206 @@
<?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;
use core_question\local\bank\question_version_status;
use mod_quiz\external\submit_question_version;
use mod_quiz\question\bank\qbank_helper;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/quiz_question_helper_test_trait.php');
/**
* Qbank helper test for quiz.
*
* @package mod_quiz
* @category test
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \mod_quiz\question\bank\qbank_helper
*/
class qbank_helper_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* @var \stdClass test student user.
*/
protected $student;
/**
* Called before every test.
*/
public function setUp(): void {
global $USER;
parent::setUp();
$this->setAdminUser();
$this->course = $this->getDataGenerator()->create_course();
$this->student = $this->getDataGenerator()->create_user();
$this->user = $USER;
}
/**
* Test reference records.
*
* @covers ::get_version_options
*/
public function test_reference_records(): void {
$this->resetAfterTest();
$quiz = $this->create_test_quiz($this->course);
// Test for questions from a different context.
$context = \context_module::instance($quiz->cmid);
// Create a couple of questions.
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
$numq = $questiongenerator->create_question('essay', null,
['category' => $cat->id, 'name' => 'This is the first version']);
// Create two version.
$questiongenerator->update_question($numq, null, ['name' => 'This is the second version']);
$questiongenerator->update_question($numq, null, ['name' => 'This is the third version']);
quiz_add_quiz_question($numq->id, $quiz);
// Create the quiz object.
$quizobj = \mod_quiz\quiz_settings::create($quiz->id);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$question = reset($questions);
$structure = structure::create_for_quiz($quizobj);
$slots = $structure->get_slots();
$slot = reset($slots);
$this->assertEquals(3, count(qbank_helper::get_version_options($question->id)));
$this->assertEquals($question->id, qbank_helper::choose_question_for_redo(
$quiz->id, $context, $slot->id, new \qubaid_list([])));
// Create another version.
$questiongenerator->update_question($numq, null, ['name' => 'This is the latest version']);
// Change to always latest.
submit_question_version::execute($slot->id, 0);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$question = reset($questions);
$this->assertEquals($question->id, qbank_helper::choose_question_for_redo(
$quiz->id, $context, $slot->id, new \qubaid_list([])));
}
/**
* Test question structure data.
*
* @covers ::get_question_structure
* @covers ::get_always_latest_version_question_ids
*/
public function test_get_question_structure(): void {
$this->resetAfterTest();
// Create a quiz.
$quiz = $this->create_test_quiz($this->course);
$quizcontext = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
// Create a question in the quiz question bank.
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category(['contextid' => $quizcontext->id]);
$q = $questiongenerator->create_question('essay', null,
['category' => $cat->id, 'name' => 'This is the first version']);
// Edit it to create a second and third version.
$questiongenerator->update_question($q, null, ['name' => 'This is the second version']);
$finalq = $questiongenerator->update_question($q, null, ['name' => 'This is the third version']);
// Add the question to the quiz.
quiz_add_quiz_question($q->id, $quiz);
// Load the quiz object and check.
$quizobj = \mod_quiz\quiz_settings::create($quiz->id);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$question = reset($questions);
$this->assertEquals($finalq->id, $question->id);
$structure = structure::create_for_quiz($quizobj);
$slots = $structure->get_slots();
$slot = reset($slots);
$this->assertEquals($finalq->id, $slot->questionid);
}
/**
* When a question only has draft versions, we should get those and not a dummy question.
*
* @return void
* @covers ::get_question_structure
*/
public function test_get_question_structure_with_drafts(): void {
$this->resetAfterTest();
// Create a quiz.
$quiz = $this->create_test_quiz($this->course);
$quizcontext = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
// Create some questions with drafts in the quiz question bank.
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category(['contextid' => $quizcontext->id]);
$q1 = $questiongenerator->create_question('essay', null,
['category' => $cat->id, 'name' => 'This is q1 the first version']);
$q2 = $questiongenerator->create_question('essay', null,
['category' => $cat->id, 'name' => 'This is q2 the first version',
'status' => question_version_status::QUESTION_STATUS_DRAFT]);
$q3 = $questiongenerator->create_question('essay', null,
['category' => $cat->id, 'name' => 'This is q3 the first version',
'status' => question_version_status::QUESTION_STATUS_DRAFT]);
// Create a new draft version of a question.
$q1final = $questiongenerator->update_question(clone $q1, null,
['name' => 'This is q1 the second version', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
$q3final = $questiongenerator->update_question(clone $q3, null,
['name' => 'This is q3 the second version', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
// Add the questions to the quiz.
quiz_add_quiz_question($q1->id, $quiz);
quiz_add_quiz_question($q2->id, $quiz);
quiz_add_quiz_question($q3->id, $quiz);
// Load the quiz object and check.
$quizobj = \mod_quiz\quiz_settings::create($quiz->id);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$this->assertCount(3, $questions);
// When a question has a Ready version, we should get that and not he draft.
$this->assertTrue(array_key_exists($q1->id, $questions));
$this->assertFalse(array_key_exists($q1final->id, $questions));
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $questions[$q1->id]->status);
$this->assertEquals('essay', $questions[$q1->id]->qtype);
// When a question only has a draft, we should get that.
$this->assertTrue(array_key_exists($q2->id, $questions));
$this->assertEquals(question_version_status::QUESTION_STATUS_DRAFT, $questions[$q2->id]->status);
$this->assertEquals('essay', $questions[$q2->id]->qtype);
// When a question has several versions but all draft, we should get the latest draft.
$this->assertFalse(array_key_exists($q3->id, $questions));
$this->assertTrue(array_key_exists($q3final->id, $questions));
$this->assertEquals(question_version_status::QUESTION_STATUS_DRAFT, $questions[$q3final->id]->status);
$this->assertEquals('essay', $questions[$q3final->id]->qtype);
}
}
@@ -0,0 +1,88 @@
<?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\question;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Unit tests for {@see display_options}.
*
* @package mod_quiz
* @category test
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\question\display_options
*/
class display_options_test extends \basic_testcase {
public function test_num_attempts_access_rule(): void {
$quiz = new \stdClass();
$quiz->decimalpoints = 2;
$quiz->questiondecimalpoints = -1;
$quiz->reviewattempt = 0x11110;
$quiz->reviewcorrectness = 0x10000;
$quiz->reviewmaxmarks = 0x10000; // Max marks is set.
$quiz->reviewmarks = 0x00000; // Marks is not set.
$quiz->reviewspecificfeedback = 0x10000;
$quiz->reviewgeneralfeedback = 0x01000;
$quiz->reviewrightanswer = 0x00100;
$quiz->reviewoverallfeedback = 0x00010;
$options = display_options::make_from_quiz($quiz,
display_options::DURING);
$this->assertEquals(true, $options->attempt);
$this->assertEquals(display_options::VISIBLE, $options->correctness);
$this->assertEquals(display_options::MAX_ONLY, $options->marks);
$this->assertEquals(display_options::VISIBLE, $options->feedback);
// The next two should be controlled by the same settings as ->feedback.
$this->assertEquals(display_options::VISIBLE, $options->numpartscorrect);
$this->assertEquals(display_options::VISIBLE, $options->manualcomment);
$this->assertEquals(2, $options->markdp);
$quiz->questiondecimalpoints = 5;
$quiz->reviewmaxmarks = 0x11000; // Max marks is set.
$quiz->reviewmarks = 0x11000; // Marks is also set.
$options = display_options::make_from_quiz($quiz,
display_options::IMMEDIATELY_AFTER);
$this->assertEquals(display_options::MARK_AND_MAX, $options->marks);
$this->assertEquals(display_options::VISIBLE, $options->generalfeedback);
$this->assertEquals(display_options::HIDDEN, $options->feedback);
// The next two should be controlled by the same settings as ->feedback.
$this->assertEquals(display_options::HIDDEN, $options->numpartscorrect);
$this->assertEquals(display_options::HIDDEN, $options->manualcomment);
$this->assertEquals(5, $options->markdp);
$quiz->reviewmaxmarks = 0x00000; // Max marks is NOT set.
$quiz->reviewmarks = 0x00000; // Marks is also NOT set.
$options = display_options::make_from_quiz($quiz,
display_options::LATER_WHILE_OPEN);
$this->assertEquals(display_options::HIDDEN, $options->marks);
$this->assertEquals(display_options::VISIBLE, $options->rightanswer);
$this->assertEquals(display_options::HIDDEN, $options->generalfeedback);
$options = display_options::make_from_quiz($quiz,
display_options::AFTER_CLOSE);
$this->assertEquals(display_options::VISIBLE, $options->overallfeedback);
$this->assertEquals(display_options::HIDDEN, $options->rightanswer);
}
}
@@ -0,0 +1,267 @@
<?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/>.
/**
* Contains the class containing unit tests for the quiz notify attempt manual grading completed cron task.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz;
use advanced_testcase;
use context_module;
use core\context;
use mod_quiz\task\quiz_notify_attempt_manual_grading_completed;
use question_engine;
use question_usage_by_activity;
use stdClass;
/**
* Class containing unit tests for the quiz notify attempt manual grading completed cron task.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_notify_attempt_manual_grading_completed_test extends advanced_testcase {
/** @var stdClass $course Test course to contain quiz. */
protected stdClass $course;
/** @var stdClass $quiz A test quiz. */
protected stdClass $quiz;
/** @var context The quiz context. */
protected context $context;
/** @var stdClass The course_module. */
protected stdClass $cm;
/** @var stdClass The student test. */
protected stdClass $student;
/** @var stdClass The student role. */
protected stdClass $studentrole;
/** @var quiz_settings Object containing the quiz settings. */
protected quiz_settings $quizobj;
/** @var question_usage_by_activity The question usage for this quiz attempt. */
protected question_usage_by_activity $quba;
/**
* Standard test setup.
*
* Create a course with a quiz and a student.
* The quiz has a truefalse question and an essay question.
*
* Also create some bits of a quiz attempt to be used later.
*/
public function setUp(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Setup test data.
$this->course = $this->getDataGenerator()->create_course();
// Create a user enrolled as a student.
$this->student = self::getDataGenerator()->create_user();
$this->studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id);
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$this->quiz = $quizgenerator->create_instance(['course' => $this->course->id, 'questionsperpage' => 0,
'grade' => 100.0, 'sumgrades' => 2]);
$this->context = context_module::instance($this->quiz->cmid);
$this->cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
// Create a truefalse question and an essay question.
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$truefalse = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
$essay = $questiongenerator->create_question('essay', null, ['category' => $cat->id]);
// Add them to the quiz.
quiz_add_quiz_question($truefalse->id, $this->quiz);
quiz_add_quiz_question($essay->id, $this->quiz);
$this->quizobj = quiz_settings::create($this->quiz->id);
$this->quba = question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context());
$this->quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour);
}
/**
* Test SQL querry get list attempt in condition.
*/
public function test_get_list_of_attempts_within_conditions(): void {
global $DB;
$timenow = time();
// Create an attempt to be completely graded (one hour ago).
$attempt1 = quiz_create_attempt($this->quizobj, 1, false, $timenow - HOURSECS, false, $this->student->id);
quiz_start_new_attempt($this->quizobj, $this->quba, $attempt1, 1, $timenow - HOURSECS);
quiz_attempt_save_started($this->quizobj, $this->quba, $attempt1);
// Process some responses from the student (30 mins ago) and submit (20 mins ago).
$attemptobj1 = quiz_attempt::create($attempt1->id);
$tosubmit = [2 => ['answer' => 'Student 1 answer', 'answerformat' => FORMAT_HTML]];
$attemptobj1->process_submitted_actions($timenow - 30 * MINSECS, false, $tosubmit);
$attemptobj1->process_finish($timenow - 20 * MINSECS, false);
// Finish the attempt of student (now).
$attemptobj1->get_question_usage()->manual_grade(2, 'Good!', 1, FORMAT_HTML);
question_engine::save_questions_usage_by_activity($attemptobj1->get_question_usage());
$update = new stdClass();
$update->id = $attemptobj1->get_attemptid();
$update->timemodified = $timenow;
$update->sumgrades = $attemptobj1->get_question_usage()->get_total_mark();
$update->gradednotificationsenttime = null; // Processfinish detects the notification is not required, so sets this.
// Unset to allow testing of our logic.
$DB->update_record('quiz_attempts', $update);
$attemptobj1->get_quizobj()->get_grade_calculator()->recompute_final_grade();
// Not quite time to send yet.
$task = new quiz_notify_attempt_manual_grading_completed();
$task->set_time_for_testing($timenow + 5 * HOURSECS - 1);
$attempts = $task->get_list_of_attempts();
$this->assertEquals(0, iterator_count($attempts));
// After time to send.
$task->set_time_for_testing($timenow + 5 * HOURSECS + 1);
$attempts = $task->get_list_of_attempts();
$this->assertEquals(1, iterator_count($attempts));
}
/**
* Test SQL query does not return attempts if the grading is not complete yet.
*/
public function test_get_list_of_attempts_without_manual_graded(): void {
$timenow = time();
// Create an attempt which won't be graded (1 hour ago).
$attempt2 = quiz_create_attempt($this->quizobj, 2, false, $timenow - HOURSECS, false, $this->student->id);
quiz_start_new_attempt($this->quizobj, $this->quba, $attempt2, 2, $timenow - HOURSECS);
quiz_attempt_save_started($this->quizobj, $this->quba, $attempt2);
// Process some responses from the student (30 mins ago) and submit (now).
$attemptobj2 = quiz_attempt::create($attempt2->id);
$tosubmit = [2 => ['answer' => 'Answer of student 2.', 'answerformat' => FORMAT_HTML]];
$attemptobj2->process_submitted_actions($timenow - 30 * MINSECS, false, $tosubmit);
$attemptobj2->process_finish($timenow, false);
// After time to notify, except attempt not graded, so it won't appear.
$task = new quiz_notify_attempt_manual_grading_completed();
$task->set_time_for_testing($timenow + 5 * HOURSECS + 1);
$attempts = $task->get_list_of_attempts();
$this->assertEquals(0, iterator_count($attempts));
}
/**
* Test notify manual grading completed task which the user attempt has not capability.
*/
public function test_notify_manual_grading_completed_task_without_capability(): void {
global $DB;
// Create an attempt for a user without the capability.
$timenow = time();
$attempt = quiz_create_attempt($this->quizobj, 3, false, $timenow, false, $this->student->id);
quiz_start_new_attempt($this->quizobj, $this->quba, $attempt, 3, $timenow - HOURSECS);
quiz_attempt_save_started($this->quizobj, $this->quba, $attempt);
// Process some responses and submit.
$attemptobj = quiz_attempt::create($attempt->id);
$tosubmit = [2 => ['answer' => 'Essay answer.', 'answerformat' => FORMAT_HTML]];
$attemptobj->process_submitted_actions($timenow - 30 * MINSECS, false, $tosubmit);
$attemptobj->process_finish($timenow - 20 * MINSECS, false);
// Grade the attempt.
$attemptobj->get_question_usage()->manual_grade(2, 'Good!', 1, FORMAT_HTML);
question_engine::save_questions_usage_by_activity($attemptobj->get_question_usage());
$update = new stdClass();
$update->id = $attemptobj->get_attemptid();
$update->timemodified = $timenow;
$update->sumgrades = $attemptobj->get_question_usage()->get_total_mark();
$update->gradednotificationsenttime = null; // Processfinish detects the notification is not required, so sets this.
// Unset to allow testing of our logic.
$DB->update_record('quiz_attempts', $update);
$attemptobj->get_quizobj()->get_grade_calculator()->recompute_final_grade();
// Run the quiz notify attempt manual graded task.
$this->expectOutputRegex("~Not sending an email because user does not have mod/quiz:emailnotifyattemptgraded capability.~");
$task = new quiz_notify_attempt_manual_grading_completed();
$task->set_time_for_testing($timenow + 5 * HOURSECS + 1);
$task->execute();
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals($attemptobj->get_attempt()->timefinish, $attemptobj->get_attempt()->gradednotificationsenttime);
}
/**
* Test notify manual grading completed task which the user attempt has capability.
*/
public function test_notify_manual_grading_completed_task_with_capability(): void {
global $DB;
// Allow student to receive messages.
assign_capability('mod/quiz:emailnotifyattemptgraded', CAP_ALLOW, $this->studentrole->id, $this->context, true);
// Create an attempt with capability.
$timenow = time();
$attempt = quiz_create_attempt($this->quizobj, 4, false, $timenow, false, $this->student->id);
quiz_start_new_attempt($this->quizobj, $this->quba, $attempt, 4, $timenow - HOURSECS);
quiz_attempt_save_started($this->quizobj, $this->quba, $attempt);
// Process some responses from the student.
$attemptobj = quiz_attempt::create($attempt->id);
$tosubmit = [2 => ['answer' => 'Answer of student.', 'answerformat' => FORMAT_HTML]];
$attemptobj->process_submitted_actions($timenow - 30 * MINSECS, false, $tosubmit);
$attemptobj->process_finish($timenow - 20 * MINSECS, false);
// Finish the attempt of student.
$attemptobj->get_question_usage()->manual_grade(2, 'Good!', 1, FORMAT_HTML);
question_engine::save_questions_usage_by_activity($attemptobj->get_question_usage());
$update = new stdClass();
$update->id = $attemptobj->get_attemptid();
$update->timemodified = $timenow;
$update->sumgrades = $attemptobj->get_question_usage()->get_total_mark();
$DB->update_record('quiz_attempts', $update);
$attemptobj->get_quizobj()->get_grade_calculator()->recompute_final_grade();
// Run the quiz notify attempt manual graded task.
$this->expectOutputRegex("~Sending email to user {$this->student->id}~");
$task = new quiz_notify_attempt_manual_grading_completed();
$task->set_time_for_testing($timenow + 5 * HOURSECS + 1);
$task->execute();
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertNotEquals(null, $attemptobj->get_attempt()->gradednotificationsenttime);
$this->assertNotEquals($attemptobj->get_attempt()->timefinish, $attemptobj->get_attempt()->gradednotificationsenttime);
}
}
@@ -0,0 +1,83 @@
<?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;
use core_question\local\bank\question_edit_contexts;
use mod_quiz\question\bank\custom_view;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/editlib.php');
/**
* Unit tests for the quiz's own question bank view class.
*
* @package mod_quiz
* @category test
* @copyright 2018 the Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_question_bank_view_test extends \advanced_testcase {
public function test_viewing_question_bank_should_not_load_individual_questions(): void {
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
/** @var core_question_generator $questiongenerator */
$questiongenerator = $generator->get_plugin_generator('core_question');
// Create a course and a quiz.
$course = $generator->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$context = \context_module::instance($quiz->cmid);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
// Create a question in the default category.
$contexts = new question_edit_contexts($context);
question_make_default_categories($contexts->all());
$cat = question_get_default_category($context->id);
$questiondata = $questiongenerator->create_question('numerical', null,
['name' => 'Example question', 'category' => $cat->id]);
// Ensure the question is not in the cache.
$cache = \cache::make('core', 'questiondata');
$cache->delete($questiondata->id);
// Generate the view.
$params = [
'qpage' => 0,
'qperpage' => 20,
'cat' => $cat->id . ',' . $context->id,
'recurse' => false,
'showhidden' => false,
'qbshowtext' => false,
'tabname' => 'editq'
];
$extraparams = ['cmid' => $cm->id];
$view = new custom_view($contexts, new \moodle_url('/'), $course, $cm, $params, $extraparams);
ob_start();
$view->display();
$html = ob_get_clean();
// Verify the output includes the expected question.
$this->assertStringContainsString('Example question', $html);
// Verify the question has not been loaded into the cache.
$this->assertFalse($cache->has($questiondata->id));
}
}
@@ -0,0 +1,207 @@
<?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/>.
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
/**
* Helper trait for quiz question unit tests.
*
* This trait helps to execute different tests for quiz, for example if it needs to create a quiz, add question
* to the question, add random quetion to the quiz, do a backup or restore.
*
* @package mod_quiz
* @category test
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait quiz_question_helper_test_trait {
/** @var \stdClass $course Test course to contain quiz. */
protected $course;
/** @var \stdClass $quiz A test quiz. */
protected $quiz;
/** @var \stdClass $user A test logged-in user. */
protected $user;
/**
* Create a test quiz for the specified course.
*
* @param \stdClass $course
* @return \stdClass
*/
protected function create_test_quiz(\stdClass $course): \stdClass {
/** @var mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
return $quizgenerator->create_instance([
'course' => $course->id,
'questionsperpage' => 0,
'grade' => 100.0,
'sumgrades' => 2,
]);
}
/**
* Helper method to add regular questions in quiz.
*
* @param component_generator_base $questiongenerator
* @param \stdClass $quiz
* @param array $override
*/
protected function add_two_regular_questions($questiongenerator, \stdClass $quiz, $override = null): void {
// Create a couple of questions.
$cat = $questiongenerator->create_question_category($override);
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
// Create another version.
$questiongenerator->update_question($saq);
quiz_add_quiz_question($saq->id, $quiz);
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
// Create two version.
$questiongenerator->update_question($numq);
$questiongenerator->update_question($numq);
quiz_add_quiz_question($numq->id, $quiz);
}
/**
* Helper method to add random question to quiz.
*
* @param component_generator_base $questiongenerator
* @param \stdClass $quiz
* @param array $override
*/
protected function add_one_random_question($questiongenerator, \stdClass $quiz, $override = []): void {
// Create a random question.
$cat = $questiongenerator->create_question_category($override);
$questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
$questiongenerator->create_question('essay', null, ['category' => $cat->id]);
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
}
/**
* Attempt questions for a quiz and user.
*
* @param \stdClass $quiz Quiz to attempt.
* @param \stdClass $user A user to attempt the quiz.
* @param int $attemptnumber
* @return array
*/
protected function attempt_quiz(\stdClass $quiz, \stdClass $user, $attemptnumber = 1): array {
$this->setUser($user);
$starttime = time();
$quizobj = quiz_settings::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);
// Start the attempt.
$attempt = quiz_create_attempt($quizobj, $attemptnumber, null, $starttime, false, $user->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $starttime);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_finish($starttime, false);
$this->setUser();
return [$quizobj, $quba, $attemptobj];
}
/**
* A helper method to backup test quiz.
*
* @param \stdClass $quiz Quiz to attempt.
* @param \stdClass $user A user to attempt the quiz.
* @return string A backup ID ready to be restored.
*/
protected function backup_quiz(\stdClass $quiz, \stdClass $user): string {
global $CFG;
// Get the necessary files to perform backup and restore.
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
$backupid = 'test-question-backup-restore';
$bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz->cmid, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $user->id);
$bc->execute_plan();
$results = $bc->get_results();
$file = $results['backup_destination'];
$fp = get_file_packer('application/vnd.moodle.backup');
$filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
$file->extract_to_pathname($fp, $filepath);
$bc->destroy();
return $backupid;
}
/**
* A helper method to restore provided backup.
*
* @param string $backupid Backup ID to restore.
* @param stdClass $course
* @param stdClass $user
*/
protected function restore_quiz(string $backupid, stdClass $course, stdClass $user): void {
$rc = new restore_controller($backupid, $course->id,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $user->id, backup::TARGET_CURRENT_ADDING);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
}
/**
* A helper method to emulate duplication of the quiz.
*
* @param stdClass $course
* @param stdClass $quiz
* @return \cm_info|null
*/
protected function duplicate_quiz($course, $quiz): ?\cm_info {
return duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid));
}
/**
* Add random questions to a quiz, with a filter condition based on a category ID.
*
* @param int $quizid The quiz to add the questions to.
* @param int $page The page number to add the questions to.
* @param int $categoryid The category ID to use for the filter condition.
* @param int $number The number of questions to add.
* @return void
*/
protected function add_random_questions(int $quizid, int $page, int $categoryid, int $number): void {
$quizobj = quiz_settings::create($quizid);
$structure = $quizobj->get_structure();
$filtercondition = [
'filter' => [
'category' => [
'jointype' => \qbank_managecategories\category_condition::JOINTYPE_DEFAULT,
'values' => [$categoryid],
'filteroptions' => ['includesubcategories' => false],
],
],
];
$structure->add_random_questions($page, $number, $filtercondition);
}
}
@@ -0,0 +1,598 @@
<?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;
use core_question\question_reference_manager;
use mod_quiz\question\display_options;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/quiz_question_helper_test_trait.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Quiz backup and restore tests.
*
* @package mod_quiz
* @category test
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_question_restore_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* @var \stdClass test student user.
*/
protected $student;
/**
* Called before every test.
*/
public function setUp(): void {
global $USER;
parent::setUp();
$this->setAdminUser();
$this->course = $this->getDataGenerator()->create_course();
$this->student = $this->getDataGenerator()->create_user();
$this->user = $USER;
}
/**
* Test a quiz backup and restore in a different course without attempts for course question bank.
*
* @covers \mod_quiz\question\bank\qbank_helper::get_question_structure
*/
public function test_quiz_restore_in_a_different_course_using_course_question_bank(): void {
$this->resetAfterTest();
// Create the test quiz.
$quiz = $this->create_test_quiz($this->course);
$oldquizcontext = \context_module::instance($quiz->cmid);
// Test for questions from a different context.
$coursecontext = \context_course::instance($this->course->id);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $coursecontext->id]);
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $coursecontext->id]);
// Make the backup.
$backupid = $this->backup_quiz($quiz, $this->user);
// Delete the current course to make sure there is no data.
delete_course($this->course, false);
// Check if the questions and associated data are deleted properly.
$this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure(
$quiz->id, $oldquizcontext)));
// Restore the course.
$newcourse = $this->getDataGenerator()->create_course();
$this->restore_quiz($backupid, $newcourse, $this->user);
// Verify.
$modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz');
$module = reset($modules);
$questions = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module->instance, $module->context);
$this->assertCount(3, $questions);
}
/**
* Test a quiz backup and restore in a different course without attempts for quiz question bank.
*
* @covers \mod_quiz\question\bank\qbank_helper::get_question_structure
*/
public function test_quiz_restore_in_a_different_course_using_quiz_question_bank(): void {
$this->resetAfterTest();
// Create the test quiz.
$quiz = $this->create_test_quiz($this->course);
// Test for questions from a different context.
$quizcontext = \context_module::instance($quiz->cmid);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
// Make the backup.
$backupid = $this->backup_quiz($quiz, $this->user);
// Delete the current course to make sure there is no data.
delete_course($this->course, false);
// Check if the questions and associated datas are deleted properly.
$this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure(
$quiz->id, $quizcontext)));
// Restore the course.
$newcourse = $this->getDataGenerator()->create_course();
$this->restore_quiz($backupid, $newcourse, $this->user);
// Verify.
$modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz');
$module = reset($modules);
$this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure(
$module->instance, $module->context)));
}
/**
* Count the questions for the context.
*
* @param int $contextid
* @param string $extracondition
* @return int the number of questions.
*/
protected function question_count(int $contextid, string $extracondition = ''): int {
global $DB;
return $DB->count_records_sql(
"SELECT COUNT(q.id)
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
JOIN {question_categories} qc on qc.id = qbe.questioncategoryid
WHERE qc.contextid = ?
$extracondition", [$contextid]);
}
/**
* Test if a duplicate does not duplicate questions in course question bank.
*
* @covers ::duplicate_module
*/
public function test_quiz_duplicate_does_not_duplicate_course_question_bank_questions(): void {
$this->resetAfterTest();
$quiz = $this->create_test_quiz($this->course);
// Test for questions from a different context.
$context = \context_course::instance($this->course->id);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $context->id]);
// Count the questions in course context.
$this->assertEquals(7, $this->question_count($context->id));
$newquiz = $this->duplicate_quiz($this->course, $quiz);
$this->assertEquals(7, $this->question_count($context->id));
$context = \context_module::instance($newquiz->id);
// Count the questions in the quiz context.
$this->assertEquals(0, $this->question_count($context->id));
}
/**
* Test quiz duplicate for quiz question bank.
*
* @covers ::duplicate_module
*/
public function test_quiz_duplicate_for_quiz_question_bank_questions(): void {
$this->resetAfterTest();
$quiz = $this->create_test_quiz($this->course);
// Test for questions from a different context.
$context = \context_module::instance($quiz->cmid);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $context->id]);
// Count the questions in course context.
$this->assertEquals(7, $this->question_count($context->id));
$newquiz = $this->duplicate_quiz($this->course, $quiz);
$this->assertEquals(7, $this->question_count($context->id));
$context = \context_module::instance($newquiz->id);
// Count the questions in the quiz context.
$this->assertEquals(7, $this->question_count($context->id));
}
/**
* Test quiz restore with attempts.
*
* @covers \mod_quiz\question\bank\qbank_helper::get_question_structure
*/
public function test_quiz_restore_with_attempts(): void {
$this->resetAfterTest();
// Create a quiz.
$quiz = $this->create_test_quiz($this->course);
$quizcontext = \context_module::instance($quiz->cmid);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
// Attempt it as a student, and check.
/** @var \question_usage_by_activity $quba */
[, $quba] = $this->attempt_quiz($quiz, $this->student);
$this->assertEquals(3, $quba->question_count());
$this->assertCount(1, quiz_get_user_attempts($quiz->id, $this->student->id));
// Make the backup.
$backupid = $this->backup_quiz($quiz, $this->user);
// Delete the current course to make sure there is no data.
delete_course($this->course, false);
// Restore the backup.
$newcourse = $this->getDataGenerator()->create_course();
$this->restore_quiz($backupid, $newcourse, $this->user);
// Verify.
$modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz');
$module = reset($modules);
$this->assertCount(1, quiz_get_user_attempts($module->instance, $this->student->id));
$this->assertCount(3, \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module->instance, $module->context));
}
/**
* Test pre 4.0 quiz restore for regular questions.
*
* Also, for efficiency, tests restore of the review options.
*
* @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
*/
public function test_pre_4_quiz_restore_for_regular_questions(): void {
global $USER, $DB;
$this->resetAfterTest();
$backupid = 'abc';
$backuppath = make_backup_temp_directory($backupid);
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
__DIR__ . "/fixtures/moodle_28_quiz.mbz", $backuppath);
// Do the restore to new course with default settings.
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
\backup::TARGET_NEW_COURSE);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
// Get the information about the resulting course and check that it is set up correctly.
$modinfo = get_fast_modinfo($newcourseid);
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
$structure = structure::create_for_quiz($quizobj);
// Verify the restored review options setting.
$this->assertEquals(display_options::DURING |
display_options::IMMEDIATELY_AFTER |
display_options::LATER_WHILE_OPEN |
display_options::AFTER_CLOSE, $quizobj->get_quiz()->reviewmaxmarks);
// Are the correct slots returned?
$slots = $structure->get_slots();
$this->assertCount(2, $slots);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$this->assertCount(2, $questions);
// Count the questions in quiz qbank.
$this->assertEquals(2, $this->question_count($quizobj->get_context()->id));
}
/**
* Test pre 4.0 quiz restore for random questions.
*
* @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
*/
public function test_pre_4_quiz_restore_for_random_questions(): void {
global $USER, $DB;
$this->resetAfterTest();
$backupid = 'abc';
$backuppath = make_backup_temp_directory($backupid);
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
__DIR__ . "/fixtures/random_by_tag_quiz.mbz", $backuppath);
// Do the restore to new course with default settings.
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
\backup::TARGET_NEW_COURSE);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
// Get the information about the resulting course and check that it is set up correctly.
$modinfo = get_fast_modinfo($newcourseid);
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
$structure = structure::create_for_quiz($quizobj);
// Are the correct slots returned?
$slots = $structure->get_slots();
$this->assertCount(1, $slots);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$this->assertCount(1, $questions);
// Count the questions for course question bank.
$this->assertEquals(6, $this->question_count(\context_course::instance($newcourseid)->id));
$this->assertEquals(6, $this->question_count(\context_course::instance($newcourseid)->id,
"AND q.qtype <> 'random'"));
// Count the questions in quiz qbank.
$this->assertEquals(0, $this->question_count($quizobj->get_context()->id));
}
/**
* Test pre 4.0 quiz restore for random question tags.
*
* @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
*/
public function test_pre_4_quiz_restore_for_random_question_tags(): void {
global $USER, $DB;
$this->resetAfterTest();
$randomtags = [
'1' => ['first question', 'one', 'number one'],
'2' => ['first question', 'one', 'number one'],
'3' => ['one', 'number one', 'second question'],
];
$backupid = 'abc';
$backuppath = make_backup_temp_directory($backupid);
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
__DIR__ . "/fixtures/moodle_311_quiz.mbz", $backuppath);
// Do the restore to new course with default settings.
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
\backup::TARGET_NEW_COURSE);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
// Get the information about the resulting course and check that it is set up correctly.
$modinfo = get_fast_modinfo($newcourseid);
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
// Count the questions in quiz qbank.
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
$this->assertEquals(2, $this->question_count($context->id));
// Are the correct slots returned?
$slots = $structure->get_slots();
$this->assertCount(3, $slots);
// Check if the tags match with the actual restored data.
foreach ($slots as $slot) {
$setreference = $DB->get_record('question_set_references',
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
$filterconditions = json_decode($setreference->filtercondition);
$tags = [];
foreach ($filterconditions->tags as $tagstring) {
$tag = explode(',', $tagstring);
$tags[] = $tag[1];
}
$this->assertEquals([], array_diff($randomtags[$slot->slot], $tags));
}
}
/**
* Test pre 4.0 quiz restore for random question used on multiple quizzes.
*
* @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
*/
public function test_pre_4_quiz_restore_shared_random_question(): void {
global $USER, $DB;
$this->resetAfterTest();
$backupid = 'abc';
$backuppath = make_backup_temp_directory($backupid);
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
__DIR__ . "/fixtures/pre-40-shared-random-question.mbz", $backuppath);
// Do the restore to new course with default settings.
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
\backup::TARGET_NEW_COURSE);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
// Get the information about the resulting course and check that it is set up correctly.
// Each quiz should contain an instance of the random question.
$modinfo = get_fast_modinfo($newcourseid);
$quizzes = $modinfo->get_instances_of('quiz');
$this->assertCount(2, $quizzes);
foreach ($quizzes as $quiz) {
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
$structure = structure::create_for_quiz($quizobj);
// Are the correct slots returned?
$slots = $structure->get_slots();
$this->assertCount(1, $slots);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$this->assertCount(1, $questions);
}
// Count the questions for course question bank.
// We should have a single question, the random question should have been deleted after the restore.
$this->assertEquals(1, $this->question_count(\context_course::instance($newcourseid)->id));
$this->assertEquals(1, $this->question_count(\context_course::instance($newcourseid)->id,
"AND q.qtype <> 'random'"));
// Count the questions in quiz qbank.
$this->assertEquals(0, $this->question_count($quizobj->get_context()->id));
}
/**
* Ensure that question slots are correctly backed up and restored with all properties.
*
* @covers \backup_quiz_activity_structure_step::define_structure()
* @return void
*/
public function test_backup_restore_question_slots(): void {
$this->resetAfterTest(true);
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
$user1 = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher');
$this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'editingteacher');
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course1->id, 'questionsperpage' => 0, 'grade' => 100.0,
'sumgrades' => 3]);
// Create some fixed and random questions.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
$matchq = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
$randomcat = $questiongenerator->create_question_category();
$questiongenerator->create_question('shortanswer', null, ['category' => $randomcat->id]);
$questiongenerator->create_question('numerical', null, ['category' => $randomcat->id]);
$questiongenerator->create_question('match', null, ['category' => $randomcat->id]);
// Add them to the quiz.
quiz_add_quiz_question($saq->id, $quiz, 1, 3);
quiz_add_quiz_question($numq->id, $quiz, 2, 2);
quiz_add_quiz_question($matchq->id, $quiz, 3, 1);
$this->add_random_questions($quiz->id, 3, $randomcat->id, 2);
$quizobj = quiz_settings::create($quiz->id, $user1->id);
$originalstructure = \mod_quiz\structure::create_for_quiz($quizobj);
// Set one slot to a non-default display number.
$originalslots = $originalstructure->get_slots();
$firstslot = reset($originalslots);
$originalstructure->update_slot_display_number($firstslot->id, rand(5, 10));
// Set one slot to requireprevious.
$lastslot = end($originalslots);
$originalstructure->update_question_dependency($lastslot->id, true);
// Backup and restore the quiz.
$backupid = $this->backup_quiz($quiz, $user1);
$this->restore_quiz($backupid, $course2, $user1);
// Ensure the restored slots match the original slots.
$modinfo = get_fast_modinfo($course2);
$quizzes = $modinfo->get_instances_of('quiz');
$restoredquiz = reset($quizzes);
$restoredquizobj = quiz_settings::create($restoredquiz->instance, $user1->id);
$restoredstructure = \mod_quiz\structure::create_for_quiz($restoredquizobj);
$restoredslots = array_values($restoredstructure->get_slots());
$originalstructure = \mod_quiz\structure::create_for_quiz($quizobj);
$originalslots = array_values($originalstructure->get_slots());
foreach ($restoredslots as $key => $restoredslot) {
$originalslot = $originalslots[$key];
$this->assertEquals($originalslot->quizid, $quiz->id);
$this->assertEquals($restoredslot->quizid, $restoredquiz->instance);
$this->assertEquals($originalslot->slot, $restoredslot->slot);
$this->assertEquals($originalslot->page, $restoredslot->page);
$this->assertEquals($originalslot->displaynumber, $restoredslot->displaynumber);
$this->assertEquals($originalslot->requireprevious, $restoredslot->requireprevious);
$this->assertEquals($originalslot->maxmark, $restoredslot->maxmark);
}
}
/**
* Test pre 4.3 quiz restore for random question filter conditions.
*
* @covers \restore_question_set_reference_data_trait::process_question_set_reference
*/
public function test_pre_43_quiz_restore_for_random_question_filtercondition(): void {
global $USER, $DB;
$this->resetAfterTest();
$backupid = 'abc';
$backuppath = make_backup_temp_directory($backupid);
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
__DIR__ . "/fixtures/moodle_42_random_question.mbz", $backuppath);
// Do the restore to new course with default settings.
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
\backup::TARGET_NEW_COURSE);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
// Get the information about the resulting course and check that it is set up correctly.
$modinfo = get_fast_modinfo($newcourseid);
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
// Count the questions in quiz qbank.
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
$this->assertEquals(2, $this->question_count($context->id));
// Are the correct slots returned?
$slots = $structure->get_slots();
$this->assertCount(1, $slots);
// Check that the filtercondition now matches the 4.3 structure.
foreach ($slots as $slot) {
$setreference = $DB->get_record('question_set_references',
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
$filterconditions = json_decode($setreference->filtercondition, true);
$this->assertArrayHasKey('cat', $filterconditions);
$this->assertArrayHasKey('jointype', $filterconditions);
$this->assertArrayHasKey('qpage', $filterconditions);
$this->assertArrayHasKey('qperpage', $filterconditions);
$this->assertArrayHasKey('filter', $filterconditions);
$this->assertArrayHasKey('category', $filterconditions['filter']);
$this->assertArrayHasKey('qtagids', $filterconditions['filter']);
$this->assertArrayHasKey('filteroptions', $filterconditions['filter']['category']);
$this->assertArrayHasKey('includesubcategories', $filterconditions['filter']['category']['filteroptions']);
// MDL-79708: Bad filter conversion check.
$this->assertArrayNotHasKey('includesubcategories', $filterconditions['filter']['category']);
$this->assertArrayNotHasKey('questioncategoryid', $filterconditions);
$this->assertArrayNotHasKey('tags', $filterconditions);
$expectedtags = \core_tag_tag::get_by_name_bulk(1, ['foo', 'bar']);
$expectedtagids = array_values(array_map(fn($expectedtag) => $expectedtag->id, $expectedtags));
$this->assertEquals($expectedtagids, $filterconditions['filter']['qtagids']['values']);
$expectedcategory = $DB->get_record('question_categories', ['idnumber' => 'RAND']);
$this->assertEquals($expectedcategory->id, $filterconditions['filter']['category']['values'][0]);
$expectedcat = implode(',', [$expectedcategory->id, $expectedcategory->contextid]);
$this->assertEquals($expectedcat, $filterconditions['cat']);
// MDL-79708: Try to convert already converted filter.
$filterconditionsold = $filterconditions;
$filterconditions = question_reference_manager::convert_legacy_set_reference_filter_condition($filterconditions);
// Check that the filtercondition didn't change.
$this->assertEquals($filterconditionsold, $filterconditions);
// MDL-79708: Try to convert a filter with previously bad conversion.
$filterconditions['filter']['category']['includesubcategories'] = 0;
unset($filterconditions['filter']['category']['filteroptions']);
$filterconditions = question_reference_manager::convert_legacy_set_reference_filter_condition($filterconditions);
$this->assertEquals($filterconditionsold, $filterconditions);
}
}
}
@@ -0,0 +1,255 @@
<?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;
use core_question\local\bank\condition;
use mod_quiz\external\submit_question_version;
use mod_quiz\question\bank\qbank_helper;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/quiz_question_helper_test_trait.php');
/**
* Question versions test for quiz.
*
* @package mod_quiz
* @category test
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\question\bank\qbank_helper
*/
class quiz_question_version_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/** @var \stdClass user record. */
protected $student;
/**
* Called before every test.
*/
public function setUp(): void {
global $USER;
parent::setUp();
$this->setAdminUser();
$this->course = $this->getDataGenerator()->create_course();
$this->student = $this->getDataGenerator()->create_user();
$this->user = $USER;
}
/**
* Test the quiz question data for changed version in the slots.
*/
public function test_quiz_questions_for_changed_versions(): void {
$this->resetAfterTest();
$quiz = $this->create_test_quiz($this->course);
// Test for questions from a different context.
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Create a couple of questions.
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
$numq = $questiongenerator->create_question('essay', null,
['category' => $cat->id, 'name' => 'This is the first version']);
// Create two version.
$questiongenerator->update_question($numq, null, ['name' => 'This is the second version']);
$questiongenerator->update_question($numq, null, ['name' => 'This is the third version']);
quiz_add_quiz_question($numq->id, $quiz);
// Create the quiz object.
$quizobj = \mod_quiz\quiz_settings::create($quiz->id);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$slots = $structure->get_slots();
$slot = reset($slots);
// Test that the version added is 'always latest'.
$this->assertEquals(3, $slot->version);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$question = reset($questions);
$this->assertEquals(3, $question->version);
$this->assertEquals('This is the third version', $question->name);
// Create another version.
$questiongenerator->update_question($numq, null, ['name' => 'This is the latest version']);
// Check that 'Always latest is working'.
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$question = reset($questions);
$this->assertEquals(4, $question->version);
$this->assertEquals('This is the latest version', $question->name);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$slots = $structure->get_slots();
$slot = reset($slots);
$this->assertEquals(4, $slot->version);
// Now change the version using the external service.
$versions = qbank_helper::get_version_options($slot->questionid);
// We don't want the current version.
$selectversions = [];
foreach ($versions as $version) {
if ($version->version === $slot->version) {
continue;
}
$selectversions [$version->version] = $version;
}
// Change to version 1.
submit_question_version::execute($slot->id, (int)$selectversions[1]->version);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$question = reset($questions);
$this->assertEquals(1, $question->version);
$this->assertEquals('This is the first version', $question->name);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$slots = $structure->get_slots();
$slot = reset($slots);
$this->assertEquals(1, $slot->version);
// Change to version 2.
submit_question_version::execute($slot->id, $selectversions[2]->version);
$quizobj->preload_questions();
$quizobj->load_questions();
$questions = $quizobj->get_questions();
$question = reset($questions);
$this->assertEquals(2, $question->version);
$this->assertEquals('This is the second version', $question->name);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$slots = $structure->get_slots();
$slot = reset($slots);
$this->assertEquals(2, $slot->version);
}
/**
* Test if changing the version of the slot changes the attempts.
*/
public function test_quiz_question_attempts_with_changed_version(): void {
$this->resetAfterTest();
$quiz = $this->create_test_quiz($this->course);
// Test for questions from a different context.
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Create a couple of questions.
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
$numq = $questiongenerator->create_question('numerical', null,
['category' => $cat->id, 'name' => 'This is the first version']);
// Create two version.
$questiongenerator->update_question($numq, null, ['name' => 'This is the second version']);
$questiongenerator->update_question($numq, null, ['name' => 'This is the third version']);
quiz_add_quiz_question($numq->id, $quiz);
[, , $attemptobj] = $this->attempt_quiz($quiz, $this->student);
$this->assertEquals('This is the third version', $attemptobj->get_question_attempt(1)->get_question()->name);
// Create the quiz object.
$quizobj = \mod_quiz\quiz_settings::create($quiz->id);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$slots = $structure->get_slots();
$slot = reset($slots);
// Now change the version using the external service.
$versions = qbank_helper::get_version_options($slot->questionid);
// We dont want the current version.
$selectversions = [];
foreach ($versions as $version) {
if ($version->version === $slot->version) {
continue;
}
$selectversions [$version->version] = $version;
}
// Change to version 1.
$this->expectException('moodle_exception');
submit_question_version::execute($slot->id, (int)$selectversions[1]->version);
[, , $attemptobj] = $this->attempt_quiz($quiz, $this->student, 2);
$this->assertEquals('This is the first version', $attemptobj->get_question_attempt(1)->get_question()->name);
// Change to version 2.
submit_question_version::execute($slot->id, (int)$selectversions[2]->version);
[, , $attemptobj] = $this->attempt_quiz($quiz, $this->student, 3);
$this->assertEquals('This is the second version', $attemptobj->get_question_attempt(1)->get_question()->name);
// Create another version.
$questiongenerator->update_question($numq, null, ['name' => 'This is the latest version']);
// Change to always latest.
submit_question_version::execute($slot->id, 0);
[, , $attemptobj] = $this->attempt_quiz($quiz, $this->student, 4);
$this->assertEquals('This is the latest version', $attemptobj->get_question_attempt(1)->get_question()->name);
}
public function test_get_version_information_for_questions_in_attempt(): void {
$this->resetAfterTest();
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Make two categories, each with a question.
$coursecontext = \context_course::instance($this->course->id);
$cat = $questiongenerator->create_question_category(
['name' => 'Non-random questions', 'context' => $coursecontext->id]);
$randomcat = $questiongenerator->create_question_category(
['name' => 'Random questions', 'context' => $coursecontext->id]);
$q1 = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
$q2 = $questiongenerator->create_question('truefalse', null, ['category' => $randomcat->id]);
// Make the quiz, adding q1, and a random question from randomcat.
$quiz = $quizgenerator->create_instance([
'course' => $this->course->id,
'grade' => 100.0,
'sumgrades' => 2,
'canredoquestions' => 1,
'preferredbehaviour' => 'immediatefeedback',
]);
$quizobj = quiz_settings::create($quiz->id);
quiz_add_quiz_question($q1->id, $quiz);
$structure = $quizobj->get_structure();
$structure->add_random_questions(0, 1, [
'filter' => [
'category' => [
'jointype' => condition::JOINTYPE_DEFAULT,
'values' => [$randomcat->id],
'filteroptions' => ['includesubcategories' => false],
],
],
]);
// Student starts attempt.
$quizobj = quiz_settings::create($quiz->id);
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
$attemptobj = quiz_attempt::create($attempt->id);
// Answer both questions.
$postdata = $questiongenerator->get_simulated_post_data_for_questions_in_usage(
$attemptobj->get_question_usage(),
[1 => 'True', 2 => 'False'],
true,
);
$attemptobj->process_submitted_actions(time(), false, $postdata);
// Redo both questions - need to re-create attemptobj each time.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_redo_question(1, time());
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_redo_question(2, time());
// Edit both questions to make a second version.
$questiongenerator->update_question($q1);
$questiongenerator->update_question($q2);
// Finally! call the method we want to test.
$versioninfo = qbank_helper::get_version_information_for_questions_in_attempt(
$attemptobj->get_attempt(), $attemptobj->get_context());
// Verify - all questions should now want to be V2 for various reasons.
$this->assertEquals(2, $versioninfo[1]->newversion);
$this->assertEquals(2, $versioninfo[2]->newversion);
$this->assertEquals(2, $versioninfo[3]->newversion);
$this->assertEquals(2, $versioninfo[4]->newversion);
}
}
+130
View File
@@ -0,0 +1,130 @@
<?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;
use basic_testcase;
use mod_quiz\question\display_options;
use mod_quiz\quiz_settings;
use stdClass;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Unit tests for the quiz class
*
* @package mod_quiz
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\quiz_settings
*/
class quizobj_test extends basic_testcase {
/**
* Test cases for {@see test_cannot_review_message()}.
*
* @return array[]
*/
public static function cannot_review_message_testcases(): array {
return [
// Review Time close
// Later close quiz attempt When Expected
// Quiz with no close date.
[false, false, null, null, display_options::DURING, ''],
[false, false, null, -60, display_options::IMMEDIATELY_AFTER, 'noreview'],
[false, false, null, -180, display_options::LATER_WHILE_OPEN, 'noreview'],
[false, false, null, -180, display_options::AFTER_CLOSE, 'noreview'],
[false, true, null, null, display_options::DURING, ''],
[false, true, null, -60, display_options::IMMEDIATELY_AFTER, 'noreview'],
[false, true, null, -180, display_options::LATER_WHILE_OPEN, 'noreview'],
[false, true, null, -180, display_options::AFTER_CLOSE, 'noreview'],
// Quiz with a close in the future date, review only after close.
[false, true, 300, null, display_options::DURING, ''],
[false, true, 300, -60, display_options::IMMEDIATELY_AFTER, 300],
[false, true, 300, -180, display_options::LATER_WHILE_OPEN, 300],
// Quiz with a close in the future date, review later while open, or after close.
[true, true, 300, null, display_options::DURING, ''],
[true, true, 300, -60, display_options::IMMEDIATELY_AFTER, 60],
[true, false, 300, -60, display_options::IMMEDIATELY_AFTER, 60],
// Quiz with no closer date, review later while open.
[true, false, 300, null, display_options::DURING, ''],
[true, false, 300, -60, display_options::IMMEDIATELY_AFTER, 60],
];
}
/**
* Unit test for {@see quiz_settings::cannot_review_message()}.
*
* @dataProvider cannot_review_message_testcases
* @param bool $reviewlater whether the quiz allows reivew 'later while the quiz is still open'.
* @param bool $reviewafterclose whether the quiz allows rievew 'after the quiz is closed'.
* @param int|null $quizcloseoffset quiz close date, relative to now. Null means not set.
* @param int|null $attemptsubmitoffset quiz attempt sumbite time relative to now. Null means not submitted yet.
* @param int $attemptstate current state of the attempt, one of the display_options constants.
* @param string|int $expectation expected result: '' means '', 'noreview' means noreview lang string,
* int means noreviewuntil with that time relative to now.
*/
public function test_cannot_review_message(
bool $reviewlater,
bool $reviewafterclose,
?int $quizcloseoffset,
?int $attemptsubmitoffset,
int $attemptstate,
string|int $expectation
): void {
$quiz = new stdClass();
$now = time();
$cm = new stdClass();
$cm->id = 123;
// Prepare quiz settings.
$quiz->reviewattempt = display_options::DURING;
if ($reviewlater) {
$quiz->reviewattempt |= display_options::LATER_WHILE_OPEN;
}
if ($reviewafterclose) {
$quiz->reviewattempt |= display_options::AFTER_CLOSE;
}
$quiz->attempts = 0;
if ($quizcloseoffset === null) {
$quiz->timeclose = 0;
} else {
$quiz->timeclose = $now + $quizcloseoffset;
}
if ($attemptsubmitoffset === null) {
$submittime = 0;
} else {
$submittime = $now + $attemptsubmitoffset;
}
$quizobj = new quiz_settings($quiz, $cm, new stdClass(), false);
// Prepare expected message.
if ($expectation === 'noreview') {
$expectation = get_string('noreview', 'quiz');
} else if (is_int($expectation)) {
$expectation = get_string('noreviewuntil', 'quiz', userdate($now + $expectation));
}
// Test.
$this->assertEquals($expectation,
$quizobj->cannot_review_message($attemptstate, false, $submittime));
}
}

Some files were not shown because too many files have changed in this diff Show More