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 2ebae5fd6ae86..22d4784550406 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;