From db3686d546f1c5f37c13cc5696a5fa6119366129 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Fri, 30 May 2014 11:39:42 -0500 Subject: [PATCH] MDL-37993 Quiz: completion upon all passing grade or attempts exhausted This patch adds completion options to Quiz similar to what is available in scorm. One can have the quiz marked complete when either a passing grade is achieved or all attempts are used up. This will allow a quiz to complete when the user "passes or fails". (Where "fail" means "using up all attempts without passing".) --- .../backup/moodle2/backup_quiz_stepslib.php | 2 +- mod/quiz/db/install.xml | 2 + mod/quiz/db/upgrade.php | 27 ++++++ mod/quiz/lang/en/quiz.php | 4 + mod/quiz/lib.php | 50 +++++++++++ mod/quiz/locallib.php | 5 ++ mod/quiz/mod_form.php | 33 +++++++ ...completion_condition_attempts_used.feature | 88 +++++++++++++++++++ ...completion_condition_passing_grade.feature | 87 ++++++++++++++++++ mod/quiz/version.php | 2 +- 10 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 mod/quiz/tests/behat/completion_condition_attempts_used.feature create mode 100644 mod/quiz/tests/behat/completion_condition_passing_grade.feature diff --git a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php index 6cd03e6daf7f2..aaa9e01b95d46 100644 --- a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php @@ -49,7 +49,7 @@ protected function define_structure() { 'questionsperpage', 'navmethod', 'shufflequestions', 'shuffleanswers', 'sumgrades', 'grade', 'timecreated', 'timemodified', 'password', 'subnet', 'browsersecurity', - 'delay1', 'delay2', 'showuserpicture', 'showblocks')); + 'delay1', 'delay2', 'showuserpicture', 'showblocks', 'completionattemptsexhausted', 'completionpass')); // Define elements for access rule subplugin settings. $this->add_subplugin_structure('quizaccess', $quiz, true); diff --git a/mod/quiz/db/install.xml b/mod/quiz/db/install.xml index 14f2fde8dc21f..85fe13d328db9 100644 --- a/mod/quiz/db/install.xml +++ b/mod/quiz/db/install.xml @@ -44,6 +44,8 @@ + + diff --git a/mod/quiz/db/upgrade.php b/mod/quiz/db/upgrade.php index 0a8ba8d45ff3c..2110673a26de7 100644 --- a/mod/quiz/db/upgrade.php +++ b/mod/quiz/db/upgrade.php @@ -776,6 +776,33 @@ function xmldb_quiz_upgrade($oldversion) { // Moodle v2.7.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2014052800) { + + // Define field completionattemptsexhausted to be added to quiz. + $table = new xmldb_table('quiz'); + $field = new xmldb_field('completionattemptsexhausted', XMLDB_TYPE_INTEGER, '1', null, null, null, '0', 'showblocks'); + + // Conditionally launch add field completionattemptsexhausted. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + // Quiz savepoint reached. + upgrade_mod_savepoint(true, 2014052800, 'quiz'); + } + + if ($oldversion < 2014052801) { + // Define field completionpass to be added to quiz. + $table = new xmldb_table('quiz'); + $field = new xmldb_field('completionpass', XMLDB_TYPE_INTEGER, '1', null, null, null, 0, 'completionattemptsexhausted'); + + // Conditionally launch add field completionpass. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Quiz savepoint reached. + upgrade_mod_savepoint(true, 2014052801, 'quiz'); + } return true; } diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index 18cc1e6b96fd8..e8462541c7fcd 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -152,6 +152,10 @@ $string['commentorgrade'] = 'Make comment or override grade'; $string['comments'] = 'Comments'; $string['completedon'] = 'Completed on'; +$string['completionpass'] = 'Require passing grade'; +$string['completionpass_help'] = 'If enabled, this activity is considered complete when the student receives a passing grade, with the pass grade set in the gradebook.'; +$string['completionattemptsexhausted'] = 'Or all available attempts completed'; +$string['completionattemptsexhausted_help'] = 'Mark quiz complete when the student has exhausted the maximum number of attempts.'; $string['configadaptive'] = 'If you choose Yes for this option then the student will be allowed multiple responses to a question even within the same attempt at the quiz.'; $string['configattemptsallowed'] = 'Restriction on the number of attempts students are allowed at the quiz.'; $string['configdecimaldigits'] = 'Number of digits that should be shown after the decimal point when displaying grades.'; diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index fdfb41835bd79..c5b9a7b49d873 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -1560,6 +1560,7 @@ function quiz_supports($feature) { case FEATURE_GROUPMEMBERSONLY: return true; case FEATURE_MOD_INTRO: return true; case FEATURE_COMPLETION_TRACKS_VIEWS: return true; + case FEATURE_COMPLETION_HAS_RULES: return true; case FEATURE_GRADE_HAS_GRADE: return true; case FEATURE_GRADE_OUTCOMES: return true; case FEATURE_BACKUP_MOODLE2: return true; @@ -1790,3 +1791,52 @@ function quiz_get_navigation_options() { QUIZ_NAVMETHOD_SEQ => get_string('navmethod_seq', 'quiz') ); } + + +/** + * Obtains the automatic completion state for this quiz on any conditions + * in quiz settings, such as if all attempts are used or a certain grade is achieved. + * + * @param object $course Course + * @param object $cm Course-module + * @param int $userid User ID + * @param bool $type Type of comparison (or/and; can be used as return value if no conditions) + * @return bool True if completed, false if not. (If no conditions, then return + * value depends on comparison type) + */ +function quiz_get_completion_state($course, $cm, $userid, $type) { + global $DB; + global $CFG; + + $quiz = $DB->get_record('quiz', array('id' => $cm->instance), '*', MUST_EXIST); + if (!$quiz->completionattemptsexhausted && !$quiz->completionpass) { + return $type; + } + + // Check if the user has used up all attempts. + if ($quiz->completionattemptsexhausted) { + $attempts = quiz_get_user_attempts($quiz->id, $userid, 'finished', true); + if ($attempts) { + $lastfinishedattempt = end($attempts); + $context = context_module::instance($cm->id); + $quizobj = quiz::create($quiz->id, $userid); + $accessmanager = new quiz_access_manager($quizobj, time(), + has_capability('mod/quiz:ignoretimelimits', $context, $userid, false)); + if ($accessmanager->is_finished(count($attempts), $lastfinishedattempt)) { + return true; + } + } + } + + // Check for passing grade. + if ($quiz->completionpass) { + require_once($CFG->libdir . '/gradelib.php'); + $item = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'mod', + 'itemmodule' => 'quiz', 'iteminstance' => $cm->instance)); + if ($item) { + $grades = grade_grade::fetch_users_grades($item, array($userid), false); + return $grades[$userid]->is_passed($item); + } + } + return false; +} diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index ffa7f86cfe588..ce078fa9abcea 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -1641,6 +1641,11 @@ function quiz_attempt_submitted_handler($event) { return true; } + // Update completion state. + $completion = new completion_info($course); + if ($completion->is_enabled($cm) && ($quiz->completionattemptsexhausted || $quiz->completionpass)) { + $completion->update_state($cm, COMPLETION_COMPLETE, $event->userid); + } return quiz_send_notification_messages($course, $quiz, $attempt, context_module::instance($cm->id), $cm); } diff --git a/mod/quiz/mod_form.php b/mod/quiz/mod_form.php index efd0e8ac81332..8475c323077e2 100644 --- a/mod/quiz/mod_form.php +++ b/mod/quiz/mod_form.php @@ -585,4 +585,37 @@ public function validation($data, $files) { return $errors; } + + /** + * Display module-specific activity completion rules. + * Part of the API defined by moodleform_mod + * @return array Array of string IDs of added items, empty array if none + */ + public function add_completion_rules() { + $mform = $this->_form; + $items = array(); + + $group = array(); + $group[] = $mform->createElement('advcheckbox', 'completionpass', null, get_string('completionpass', 'quiz'), + array('group' => 'cpass')); + + $group[] = $mform->createElement('advcheckbox', 'completionattemptsexhausted', null, + get_string('completionattemptsexhausted', 'quiz'), + array('group' => 'cattempts')); + $mform->disabledIf('completionattemptsexhausted', 'completionpass', 'notchecked'); + $mform->addGroup($group, 'completionpassgroup', get_string('completionpass', 'quiz'), '', false); + $mform->addHelpButton('completionpassgroup', 'completionpass', 'quiz'); + $items[] = 'completionpassgroup'; + return $items; + } + + /** + * Called during validation. Indicates whether a module-specific completion rule is selected. + * + * @param array $data Input data (not yet validated) + * @return bool True if one or more rules is enabled, false if none are. + */ + public function completion_rule_enabled($data) { + return !empty($data['completionattemptsexhausted']) || !empty($data['completionpass']); + } } diff --git a/mod/quiz/tests/behat/completion_condition_attempts_used.feature b/mod/quiz/tests/behat/completion_condition_attempts_used.feature new file mode 100644 index 0000000000000..d1684dd64043e --- /dev/null +++ b/mod/quiz/tests/behat/completion_condition_attempts_used.feature @@ -0,0 +1,88 @@ +@mod @mod_quiz +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@asd.com | + | teacher1 | Teacher | 1 | teacher1@asd.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 I log in as "admin" + And I set the following administration settings values: + | Enable completion tracking | 1 | + And I expand "Grades" node + And I follow "Grade item settings" + And I set the field "Advanced grade item options" to "hiddenuntil" + And I press "Save changes" + And I log out + + Scenario: student1 uses up both attempts without passing + When I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I click on "Edit settings" "link" in the "Administration" "block" + And I set the following fields to these values: + | Enable completion tracking | Yes | + And I press "Save changes" + And I add a "Quiz" to section "1" and I fill the form with: + | Name | Test quiz name | + | Description | Test quiz description | + | Completion tracking | Show activity as complete when conditions are met | + | Attempts allowed | 2 | + | Require passing grade | 1 | + | Or all available attempts completed | 1 | + 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 | True | + | Feedback for the response 'True'. | So you think it is true | + | Feedback for the response 'False'. | So you think it is false | + And I follow "Course 1" + And I follow "Grades" + And I follow "Simple view" + And I follow "Edit quiz Test quiz name" + Then I should see "Edit grade item" + And I set the field "gradepass" to "5" + And I press "Save changes" + And I should see "Simple view" + Then I log out + + And I log in as "student1" + And I follow "Course 1" + And "//img[contains(@alt, 'Not completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element" + And I follow "Test quiz name" + And I press "Attempt quiz now" + And I should see "Question 1" + And I should see "Answer the first question" + And I set the field "False" to "1" + And I press "Next" + And I should see "Answer saved" + And I press "Submit all and finish" + And I follow "C1" + And "//img[contains(@alt, 'Not completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element" + And I follow "Test quiz name" + And I press "Re-attempt quiz" + Then I should see "Question 1" + And I should see "Answer the first question" + And I set the field "False" to "1" + And I press "Next" + And I should see "Answer saved" + And I press "Submit all and finish" + And I follow "C1" + And "//img[contains(@alt, 'Completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element" + And I log out + And I log in as "teacher1" + And I follow "Course 1" + And I follow "Activity completion" + Then "//img[contains(@title,'Test quiz name') and @alt='Completed']" "xpath_element" should exist in the "Student 1" "table_row" + diff --git a/mod/quiz/tests/behat/completion_condition_passing_grade.feature b/mod/quiz/tests/behat/completion_condition_passing_grade.feature new file mode 100644 index 0000000000000..38fa5d3dbcde5 --- /dev/null +++ b/mod/quiz/tests/behat/completion_condition_passing_grade.feature @@ -0,0 +1,87 @@ +@mod @mod_quiz +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@asd.com | + | teacher1 | Teacher | 1 | teacher1@asd.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 I log in as "admin" + And I set the following administration settings values: + | Enable completion tracking | 1 | + And I expand "Grades" node + And I follow "Grade item settings" + And I set the field "Advanced grade item options" to "hiddenuntil" + And I press "Save changes" + And I log out + + Scenario: student1 passes on the first try + When I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I click on "Edit settings" "link" in the "Administration" "block" + And I set the following fields to these values: + | Enable completion tracking | Yes | + And I press "Save changes" + And I add a "Quiz" to section "1" and I fill the form with: + | Name | Test quiz name | + | Description | Test quiz description | + | Completion tracking | Show activity as complete when conditions are met | + | Attempts allowed | 4 | + | Require passing grade | 1 | + 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 | True | + | Feedback for the response 'True'. | So you think it is true | + | Feedback for the response 'False'. | So you think it is false | + And I follow "Course 1" + And I follow "Grades" + And I select "Simple view" from "jump" + And I press "Go" + And I follow "Edit quiz Test quiz name" + Then I should see "Edit grade item" + And I set the field "gradepass" to "5" + And I press "Save changes" + Then I should see "Simple view" + And I log out + + And I log in as "student1" + And I follow "Course 1" + And "//img[contains(@alt, 'Not completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element" + And I follow "Test quiz name" + And I press "Attempt quiz now" + Then I should see "Question 1" + And I should see "Answer the first question" + And I set the field "False" to "1" + And I press "Next" + And I should see "Answer saved" + And I press "Submit all and finish" + And I follow "C1" + And "//img[contains(@alt, 'Not completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element" + And I follow "Test quiz name" + And I press "Re-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 "Next" + And I should see "Answer saved" + And I press "Submit all and finish" + And I follow "C1" + And "//img[contains(@alt, 'Completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element" + And I log out + And I log in as "teacher1" + And I follow "Course 1" + And I follow "Activity completion" + Then "//img[contains(@title,'Test quiz name') and @alt='Completed']" "xpath_element" should exist in the "Student 1" "table_row" diff --git a/mod/quiz/version.php b/mod/quiz/version.php index 06828f87529b4..0d368c8842766 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2014051200; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2014052801; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2014050800; // Requires this Moodle version. $plugin->component = 'mod_quiz'; // Full name of the plugin (used for diagnostics). $plugin->cron = 60;