diff --git a/completion/classes/bulkedit_form.php b/completion/classes/bulkedit_form.php index d17ae8ab76d..27020dd4348 100644 --- a/completion/classes/bulkedit_form.php +++ b/completion/classes/bulkedit_form.php @@ -53,6 +53,20 @@ class core_completion_bulkedit_form extends core_completion_edit_base_form { return $this->_modnames; } + /** + * It will return the course module when $cms has only one course module; otherwise, null will be returned. + * + * @return \stdClass|null + */ + protected function get_cm(): ?\stdClass { + if (count($this->cms) === 1) { + return reset($this->cms)->get_course_module_record(); + } + + // If there are multiple modules, so none will be selected. + return null; + } + /** * Returns an instance of component-specific module form for the first selected module * @@ -156,4 +170,4 @@ class core_completion_bulkedit_form extends core_completion_edit_base_form { return $errors; } -} \ No newline at end of file +} diff --git a/completion/classes/defaultedit_form.php b/completion/classes/defaultedit_form.php index 7880a727a06..ec088370f7c 100644 --- a/completion/classes/defaultedit_form.php +++ b/completion/classes/defaultedit_form.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Default activity completion form - * - * @package core_completion - * @copyright 2017 Marina Glancy - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die; - /** * Default activity completion form * @@ -121,4 +111,14 @@ class core_completion_defaultedit_form extends core_completion_edit_base_form { $this->set_data($data); } } -} \ No newline at end of file + + /** + * There is no course module for this form, because it is used to update default completion settings. So it will + * always return null. + * + * @return \stdClass|null + */ + protected function get_cm(): ?\stdClass { + return null; + } +} diff --git a/completion/classes/edit_base_form.php b/completion/classes/edit_base_form.php index 903a51ab00a..4d41d5ccaac 100644 --- a/completion/classes/edit_base_form.php +++ b/completion/classes/edit_base_form.php @@ -14,18 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Base form for changing completion rules - * - * @package core_completion - * @copyright 2017 Marina Glancy - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir.'/formslib.php'); -require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->dirroot.'/course/modlib.php'); /** @@ -36,6 +27,9 @@ require_once($CFG->dirroot.'/course/modlib.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class core_completion_edit_base_form extends moodleform { + + use \core_completion\form\form_trait; + /** @var moodleform_mod Do not use directly, call $this->get_module_form() */ protected $_moduleform = null; /** @var bool */ @@ -50,6 +44,21 @@ abstract class core_completion_edit_base_form extends moodleform { */ abstract protected function get_module_names(); + /** + * Get the module name. If the form have more than one modules, it will return the first one. + * + * @return string|null The module name or null if there is no modules associated to this form. + */ + protected function get_module_name(): ?string { + $modnames = $this->get_module_names(); + if (empty($modnames)) { + return null; + } + + $modnamekeys = array_keys($modnames); + return reset($modnamekeys); + } + /** * Returns true if all selected modules support tracking view. * @@ -90,7 +99,7 @@ abstract class core_completion_edit_base_form extends moodleform { * * @return array */ - protected function add_custom_completion_rules() { + protected function add_completion_rules() { $modnames = array_keys($this->get_module_names()); if (count($modnames) != 1 || !plugin_supports('mod', $modnames[0], FEATURE_COMPLETION_HAS_RULES, false)) { return []; @@ -178,61 +187,12 @@ abstract class core_completion_edit_base_form extends moodleform { $mform->addElement('hidden', 'id', $this->course->id); $mform->setType('id', PARAM_INT); - // Unlock completion automatically (this element can be used in validation). - $mform->addElement('hidden', 'completionunlocked', 1); - $mform->setType('completionunlocked', PARAM_INT); - - $mform->addElement('select', 'completion', get_string('completion', 'completion'), - array(COMPLETION_TRACKING_NONE => get_string('completion_none', 'completion'), - COMPLETION_TRACKING_MANUAL => get_string('completion_manual', 'completion'))); - $mform->addHelpButton('completion', 'completion', 'completion'); - $mform->setDefault('completion', COMPLETION_TRACKING_NONE); - - // Automatic completion once you view it. - $autocompletionpossible = false; - if ($this->support_views()) { - $mform->addElement('advcheckbox', 'completionview', get_string('completionview', 'completion'), - get_string('completionview_desc', 'completion')); - $mform->disabledIf('completionview', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); - $autocompletionpossible = true; - } - - // Automatic completion once it's graded. - if ($this->support_grades()) { - $group = []; - $group[] = $mform->createElement('advcheckbox', 'completionusegrade', get_string('completionusegrade', 'completion'), - get_string('completionusegrade_desc', 'completion')); - $mform->addHelpButton('completionusegrade', 'completionusegrade', 'completion', '', true); - - $group[] = $mform->createElement('advcheckbox', 'completionpassgrade', get_string('completionpassgrade', 'completion'), - get_string('completionpassgrade_desc', 'completion')); - $mform->addHelpButton('completionpassgrade', 'completionpassgrade', 'completion', '', true); - $mform->disabledIf('completionpassgrade', 'completionusegrade', 'notchecked'); - - $mform->addGroup($group, 'completionpassgroup', get_string('completionpassgrade', 'completion'), '   ', false); - $mform->disabledIf('completionpassgroup', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); - - $autocompletionpossible = true; - } - - // Automatic completion according to module-specific rules. - foreach ($this->add_custom_completion_rules() as $element) { - $mform->disabledIf($element, 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); - $autocompletionpossible = true; - } - - // Automatic option only appears if possible. - if ($autocompletionpossible) { - $mform->getElement('completion')->addOption( - get_string('completion_automatic', 'completion'), - COMPLETION_TRACKING_AUTOMATIC); - } - - // Completion expected at particular date? (For progress tracking). - $mform->addElement('date_time_selector', 'completionexpected', - get_string('completionexpected', 'completion'), ['optional' => true]); - $mform->addHelpButton('completionexpected', 'completionexpected', 'completion'); - $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE); + // Add the completion elements to the form. + $this->add_completion_elements( + $this->get_module_name(), + $this->support_views(), + $this->support_grades() + ); if ($conflicts = $this->get_modules_with_hidden_rules()) { $mform->addElement('static', 'qwerty', '', get_string('hiddenrules', 'completion', join(', ', $conflicts))); @@ -241,6 +201,13 @@ abstract class core_completion_edit_base_form extends moodleform { $this->add_action_buttons(); } + /** + * Each module which defines definition_after_data() must call this method. + */ + public function definition_after_data() { + $this->definition_after_data_completion(); + } + /** * Form validation * @@ -252,15 +219,8 @@ abstract class core_completion_edit_base_form extends moodleform { public function validation($data, $files) { $errors = parent::validation($data, $files); - // Completion: Don't let them choose automatic completion without turning - // on some conditions. - if (array_key_exists('completion', $data) && - $data['completion'] == COMPLETION_TRACKING_AUTOMATIC) { - if (empty($data['completionview']) && empty($data['completionusegrade']) && empty($data['completionpassgrade']) && - !$this->completion_rule_enabled($data)) { - $errors['completion'] = get_string('badautocompletion', 'completion'); - } - } + // Completion: Check completion fields don't have errors. + $errors = array_merge($errors, $this->validate_completion($data)); return $errors; } @@ -288,4 +248,4 @@ abstract class core_completion_edit_base_form extends moodleform { } return $data; } -} \ No newline at end of file +} diff --git a/completion/classes/form/form_trait.php b/completion/classes/form/form_trait.php new file mode 100644 index 00000000000..9faf5b23d66 --- /dev/null +++ b/completion/classes/form/form_trait.php @@ -0,0 +1,384 @@ +. + +namespace core_completion\form; + +use core_grades\component_gradeitems; + +/** + * Completion trait helper, with methods to add completion elements and validate them. + * + * @package core_completion + * @since Moodle 4.3 + * @copyright 2023 Sara Arjona (sara@moodle.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait form_trait { + + /** + * Called during validation. + * Override this method to indicate, based on the data, whether a custom completion rule is selected or not. + * + * @param array $data Input data (not yet validated) + * @return bool True if one or more rules are enabled; false if none are. + */ + abstract protected function completion_rule_enabled($data); + + /** + * Add completion elements to the form and return the list of element ids. + * + * @return array Array of string IDs of added items, empty array if none + */ + abstract protected function add_completion_rules(); + + /** + * Get the form associated to this class, where the completion elements will be added. + * This method must be overriden by the class using this trait if it doesn't include a _form property. + * + * @return \MoodleQuickForm + * @throws \coding_exception If the class does not have a _form property. + */ + protected function get_form(): \MoodleQuickForm { + if (property_exists($this, '_form')) { + return $this->_form; + } + + throw new \coding_exception('This class does not have a _form property. Please, add it or override the get_form() method.'); + } + + /** + * Get the cm (course module) associated to this class. + * This method must be overriden by the class using this trait if it doesn't include a _cm property. + * + * @return \stdClass|null + * @throws \coding_exception If the class does not have a _cm property. + */ + protected function get_cm(): ?\stdClass { + if (property_exists($this, '_cm')) { + return $this->_cm; + } + + throw new \coding_exception('This class does not have a _cm property. Please, add it or override the get_cm() method.'); + } + + /** + * Add completion elements to the form. + * + * @param string|null $modname The module name (for example, 'assign'). If null and form is moodleform_mod, the parameters are + * overriden with the expected values from the form. + * @param bool $supportviews True if the module supports views and false otherwise. + * @param bool $supportgrades True if the module supports grades and false otherwise. + * @param bool $rating True if the rating feature is enabled and false otherwise. + * @param bool $defaultcompletion True if the default completion is enabled and false otherwise. To review in MDL-78531. + * @throws \coding_exception If the form is not moodleform_mod and $modname is null. + */ + protected function add_completion_elements( + string $modname = null, + bool $supportviews = false, + bool $supportgrades = false, + bool $rating = false, + bool $defaultcompletion = true + ): void { + global $CFG; + + $mform = $this->get_form(); + if ($modname === null) { + if ($this instanceof \moodleform_mod) { + // By default, all the modules can be initiatized with the same parameters. + $modname = $this->_modname; + $supportviews = plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false); + $supportgrades = plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false); + $rating = $this->_features->rating; + $defaultcompletion = $CFG->completiondefault && $this->_features->defaultcompletion; + } else { + throw new \coding_exception('You must specify the modname parameter if you are not using a moodleform_mod.'); + } + } + + // Unlock button if people have completed it. The button will be removed later in definition_after_data if they haven't. + $mform->addElement('submit', 'unlockcompletion', get_string('unlockcompletion', 'completion')); + $mform->registerNoSubmitButton('unlockcompletion'); + $mform->addElement('hidden', 'completionunlocked', 0); + $mform->setType('completionunlocked', PARAM_INT); + + $trackingdefault = COMPLETION_TRACKING_NONE; + // If system and activity default completion is on, set it. + if ($defaultcompletion) { + $hasrules = plugin_supports('mod', $modname, FEATURE_COMPLETION_HAS_RULES, true); + if ($hasrules || $supportviews) { + $trackingdefault = COMPLETION_TRACKING_AUTOMATIC; + } else { + $trackingdefault = COMPLETION_TRACKING_MANUAL; + } + } + + $mform->addElement( + 'select', + 'completion', + get_string('completion', 'completion'), + [ + COMPLETION_TRACKING_NONE => get_string('completion_none', 'completion'), + COMPLETION_TRACKING_MANUAL => get_string('completion_manual', 'completion'), + ] + ); + $mform->setDefault('completion', $trackingdefault); + $mform->addHelpButton('completion', 'completion', 'completion'); + + // Automatic completion once you view it. + $autocompletionpossible = false; + if ($supportviews) { + $mform->addElement('checkbox', 'completionview', get_string('completionview', 'completion'), + get_string('completionview_desc', 'completion')); + $mform->hideIf('completionview', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + // Check by default if automatic completion tracking is set. + if ($trackingdefault == COMPLETION_TRACKING_AUTOMATIC) { + $mform->setDefault('completionview', 1); + } + $autocompletionpossible = true; + } + + // If the activity supports grading, the grade elements must be added. + if ($supportgrades) { + $autocompletionpossible = true; + $this->add_completiongrade_elements($modname, $rating); + } + + // Automatic completion according to module-specific rules. + $customcompletionelements = $this->add_completion_rules(); + if (property_exists($this, '_customcompletionelements')) { + $this->_customcompletionelements = $customcompletionelements; + } + + if ($customcompletionelements !== null) { + foreach ($customcompletionelements as $element) { + $mform->hideIf($element, 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + } + $autocompletionpossible = $autocompletionpossible || count($customcompletionelements) > 0; + } + + // Automatic option only appears if possible. + if ($autocompletionpossible) { + $mform->getElement('completion')->addOption( + get_string('completion_automatic', 'completion'), + COMPLETION_TRACKING_AUTOMATIC); + } + + // Completion expected at particular date? (For progress tracking). + $mform->addElement('date_time_selector', 'completionexpected', get_string('completionexpected', 'completion'), + ['optional' => true]); + $mform->addHelpButton('completionexpected', 'completionexpected', 'completion'); + $mform->hideIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE); + } + + /** + * Add completion grade elements to the form. + * + * @param string $modname The name of the module (for example, 'assign'). + * @param bool $rating True if the rating feature is enabled and false otherwise. + */ + protected function add_completiongrade_elements( + string $modname, + bool $rating = false + ): void { + $mform = $this->get_form(); + + $completionelementexists = $mform->elementExists('completion'); + $component = "mod_{$modname}"; + $itemnames = component_gradeitems::get_itemname_mapping_for_component($component); + if (count($itemnames) === 1) { + // Only one gradeitem in this activity. + // We use the completionusegrade field here. + $mform->addElement( + 'checkbox', + 'completionusegrade', + get_string('completionusegrade', 'completion'), + get_string('completionusegrade_desc', 'completion') + ); + $mform->addHelpButton('completionusegrade', 'completionusegrade', 'completion'); + + // Complete if the user has reached the pass grade. + $mform->addElement( + 'checkbox', + 'completionpassgrade', null, + get_string('completionpassgrade_desc', 'completion') + ); + $mform->disabledIf('completionpassgrade', 'completionusegrade', 'notchecked'); + $mform->addHelpButton('completionpassgrade', 'completionpassgrade', 'completion'); + + if ($completionelementexists) { + $mform->hideIf('completionpassgrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + $mform->hideIf('completionusegrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + } + + // The disabledIf logic differs between ratings and other grade items due to different field types. + if ($rating) { + // If using the rating system, there is no grade unless ratings are enabled. + $mform->disabledIf('completionusegrade', 'assessed', 'eq', 0); + $mform->disabledIf('completionpassgrade', 'assessed', 'eq', 0); + } else { + // All other field types use the '$gradefieldname' field's modgrade_type. + $itemnumbers = array_keys($itemnames); + $itemnumber = array_shift($itemnumbers); + $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade'); + $mform->disabledIf('completionusegrade', "{$gradefieldname}[modgrade_type]", 'eq', 'none'); + $mform->disabledIf('completionpassgrade', "{$gradefieldname}[modgrade_type]", 'eq', 'none'); + } + } else if (count($itemnames) > 1) { + // There are multiple grade items in this activity. + // Show them all. + $options = [ + '' => get_string('activitygradenotrequired', 'completion'), + ]; + foreach ($itemnames as $itemnumber => $itemname) { + $options[$itemnumber] = get_string("grade_{$itemname}_name", $component); + } + + $mform->addElement( + 'select', + 'completiongradeitemnumber', + get_string('completionusegrade', 'completion'), + $options + ); + + // Complete if the user has reached the pass grade. + $mform->addElement( + 'checkbox', + 'completionpassgrade', null, + get_string('completionpassgrade_desc', 'completion') + ); + $mform->disabledIf('completionpassgrade', 'completiongradeitemnumber', 'eq', ''); + $mform->addHelpButton('completionpassgrade', 'completionpassgrade', 'completion'); + + if ($completionelementexists) { + $mform->hideIf('completiongradeitemnumber', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + $mform->hideIf('completionpassgrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + } + } + } + + /** + * Perform some extra validation for completion settings. + * + * @param array $data Array of ["fieldname" => value] of submitted data. + * @return array List of ["element_name" => "error_description"] if there are errors or an empty array if everything is OK. + */ + protected function validate_completion(array $data): array { + $errors = []; + + // Completion: Don't let them choose automatic completion without turning on some conditions. + $automaticcompletion = array_key_exists('completion', $data) && $data['completion'] == COMPLETION_TRACKING_AUTOMATIC; + // Ignore this check when completion settings are locked, as the options are then disabled. + $automaticcompletion = $automaticcompletion && !empty($data['completionunlocked']); + if ($automaticcompletion) { + // View to complete. + $rulesenabled = !empty($data['completionview']); + + // Use grade to complete (only one grade item). + $rulesenabled = $rulesenabled || !empty($data['completionusegrade']) || !empty($data['completionpassgrade']); + + // Use grade to complete (specific grade item). + if (!$rulesenabled && isset($data['completiongradeitemnumber'])) { + $rulesenabled = $data['completiongradeitemnumber'] != ''; + } + + // Module-specific completion rules. + $rulesenabled = $rulesenabled || $this->completion_rule_enabled($data); + + if (!$rulesenabled) { + // No rules are enabled. Can't set automatically completed without rules. + $errors['completion'] = get_string('badautocompletion', 'completion'); + } + } + + return $errors; + } + + /** + * It should be called from the definition_after_data() to setup the completion settings in the form. + */ + protected function definition_after_data_completion(): void { + global $COURSE; + + $mform = $this->get_form(); + + $completion = new \completion_info($COURSE); + if ($completion->is_enabled()) { + // If anybody has completed the activity, these options will be 'locked'. + $cm = $this->get_cm(); + $completedcount = empty($cm) ? 0 : $completion->count_user_data($cm); + $freeze = false; + if (!$completedcount) { + if ($mform->elementExists('unlockcompletion')) { + $mform->removeElement('unlockcompletion'); + } + // Automatically set to unlocked. Note: this is necessary in order to make it recalculate completion once + // the option is changed, maybe someone has completed it now. + $mform->getElement('completionunlocked')->setValue(1); + } else { + // Has the element been unlocked, either by the button being pressed in this request, or the field already + // being set from a previous one? + if ($mform->exportValue('unlockcompletion') || $mform->exportValue('completionunlocked')) { + // Yes, add in warning text and set the hidden variable. + $completedunlockedel = $mform->createElement( + 'static', + 'completedunlocked', + get_string('completedunlocked', 'completion'), + get_string('completedunlockedtext', 'completion') + ); + $mform->insertElementBefore($completedunlockedel, 'unlockcompletion'); + $mform->removeElement('unlockcompletion'); + $mform->getElement('completionunlocked')->setValue(1); + } else { + // No, add in the warning text with the count (now we know it) before the unlock button. + $completedwarningel = $mform->createElement( + 'static', + 'completedwarning', + get_string('completedwarning', 'completion'), + get_string('completedwarningtext', 'completion', $completedcount) + ); + $mform->insertElementBefore($completedwarningel, 'unlockcompletion'); + $freeze = true; + } + } + + if ($freeze) { + $mform->freeze('completion'); + if ($mform->elementExists('completionview')) { + // Don't use hardFreeze or checkbox value gets lost. + $mform->freeze('completionview'); + } + if ($mform->elementExists('completionusegrade')) { + $mform->freeze('completionusegrade'); + } + if ($mform->elementExists('completionpassgrade')) { + $mform->freeze('completionpassgrade'); + + // Has the completion pass grade completion criteria been set? If it has, then we shouldn't change + // the gradepass field. + if ($mform->exportValue('completionpassgrade')) { + $mform->freeze('gradepass'); + } + } + if ($mform->elementExists('completiongradeitemnumber')) { + $mform->freeze('completiongradeitemnumber'); + } + if (property_exists($this, '_customcompletionelements')) { + $mform->freeze($this->_customcompletionelements); + } + } + } + } +} diff --git a/completion/tests/behat/bulk_edit_activity_completion.feature b/completion/tests/behat/bulk_edit_activity_completion.feature index 60f955a01b0..4f12ab1a43a 100644 --- a/completion/tests/behat/bulk_edit_activity_completion.feature +++ b/completion/tests/behat/bulk_edit_activity_completion.feature @@ -43,8 +43,8 @@ Feature: Allow teachers to bulk edit activity completion rules in a course. And I click on "Edit" "button" And I should see "Completion tracking" And I should see "The changes will affect the following 2 activities or resources:" - And I should see "Student must make a submission" And I select "Show activity as complete when conditions are met" from the "completion" singleselect + And I should see "Student must make a submission" And I click on "completionview" "checkbox" And I click on "completionusegrade" "checkbox" And I click on "completionsubmit" "checkbox" @@ -74,8 +74,8 @@ Feature: Allow teachers to bulk edit activity completion rules in a course. And I click on "Edit" "button" And I should see "Completion tracking" And I should see "The changes will affect the following 2 activities or resources:" - And I should see "Student must make a submission" And I select "Show activity as complete when conditions are met" from the "completion" singleselect + And I should see "Student must make a submission" And I click on "completionusegrade" "checkbox" And I click on "completionpassgrade" "checkbox" And I click on "Save changes" "button" diff --git a/completion/tests/behat/default_activity_completion.feature b/completion/tests/behat/default_activity_completion.feature index fec208894fb..fce5fad9b09 100644 --- a/completion/tests/behat/default_activity_completion.feature +++ b/completion/tests/behat/default_activity_completion.feature @@ -1,50 +1,152 @@ -@core @core_completion +@core @core_completion @javascript Feature: Allow teachers to edit the default activity completion rules in a course. In order to set the activity completion defaults for new activities As a teacher I need to be able to edit the completion rules for a group of activities. - # Given I am a teacher in a course with completion tracking enabled and activities present. - # When I edit activity completion defaults for activity types. - # Then the completion rule defaults should apply only to activities created from that point onwards. - @javascript - Scenario: Bulk edit activity completion default rules + Background: Given the following "courses" exist: - | fullname | shortname | category | - | Course 1 | C1 | 0 | + | fullname | shortname | category | enablecompletion | + | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | - | student1 | Student | First | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | - | student1 | C1 | student | - And the following "activities" exist: - | activity | course | idnumber | name | intro | grade | - | assign | C1 | a1 | Test assignment one | Submit something! | 300 | - And I log in as "teacher1" - And I am on "Course 1" course homepage with editing mode on - And I navigate to "Settings" in current page administration - And I set the following fields to these values: - | Enable completion tracking | Yes | - And I press "Save and display" + + # Given I am a teacher in a course with completion tracking enabled and activities present. + # When I edit activity completion defaults for activity types. + # Then the completion rule defaults should apply only to activities created from that point onwards. + Scenario: Edit default activity completion rules for assignment + Given the following "activity" exists: + | activity | assign | + | course | C1 | + | name | Test assignment one | + | completion | 0 | + And I am on the "Course 1" course page logged in as teacher1 When I navigate to "Course completion" in current page administration And I set the field "Course completion tertiary navigation" to "Default activity completion" And I click on "Assignments" "checkbox" And I click on "Edit" "button" And I should see "Completion tracking" And I should see "The changes will affect the following 1 activities or resources:" - And I should see "Student must make a submission" And I set the following fields to these values: - | completion | Show activity as complete when conditions are met| - | completionview | 1 | - | completionusegrade | 1 | - | completionsubmit | 1 | + | completion | Show activity as complete when conditions are met | + | completionview | 0 | + | completionusegrade | 1 | + | completionsubmit | 1 | And I click on "Save changes" "button" Then I should see "Changes saved" And I should see "With conditions" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element" - And I should see "Student must view this activity to complete it" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element" + And I should not see "Student must view this activity to complete it" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element" And I should see "Student must receive a grade to complete this activity" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element" And I should see "Student must make a submission" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element" And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element" + And I am on "Course 1" course homepage with editing mode on + And I press "Add an activity or resource" + And I click on "Add a new Assignment" "link" in the "Add an activity or resource" "dialogue" + And I expand all fieldsets + # Completion tracking 2 = Show activity as complete when conditions are met. + And the field "Completion tracking" matches value "2" + And the field "completionview" matches value "0" + And the field "completionusegrade" matches value "1" + And the field "completionsubmit" matches value "1" + But I am on the "Test assignment one" Activity page + And I navigate to "Settings" in current page administration + And I expand all fieldsets + # Completion tracking 0 = Do not indicate activity completion. + And the field "Completion tracking" matches value "0" + + Scenario: Edit default activity completion rules for forum + Given the following "activity" exists: + | activity | forum | + | course | C1 | + | name | Test forum one | + | completion | 0 | + And I am on the "Course 1" course page logged in as teacher1 + When I navigate to "Course completion" in current page administration + And I select "Default activity completion" from the "Course completion tertiary navigation" singleselect + And I click on "Forums" "checkbox" + And I click on "Edit" "button" + And I set the following fields to these values: + | completion | Show activity as complete when conditions are met | + | completionview | 0 | + # 0 = Rating. + | completiongradeitemnumber | 0 | + | completionpassgrade | 1 | + | completionpostsenabled | 1 | + | completionposts | 2 | + | completionrepliesenabled | 1 | + | completionreplies | 3 | + And I click on "Save changes" "button" + Then I should see "Changes saved" + And I am on "Course 1" course homepage with editing mode on + And I press "Add an activity or resource" + And I click on "Add a new Forum" "link" in the "Add an activity or resource" "dialogue" + And I expand all fieldsets + # Completion tracking 2 = Show activity as complete when conditions are met. + And the field "Completion tracking" matches value "2" + And the field "completionview" matches value "0" + # Value 0 for completiongradeitemnumber is "Rating". + And the field "completiongradeitemnumber" matches value "0" + And the field "completionpassgrade" matches value "1" + And the field "completionpostsenabled" matches value "1" + And the field "completionposts" matches value "2" + And the field "completionrepliesenabled" matches value "1" + And the field "completionreplies" matches value "3" + But I am on the "Test forum one" Activity page + And I navigate to "Settings" in current page administration + And I expand all fieldsets + # Completion tracking 0 = Do not indicate activity completion. + And the field "Completion tracking" matches value "0" + + Scenario: Edit default activity completion rules for glossary + Given the following "activity" exists: + | activity | glossary | + | course | C1 | + | name | Test glossary one | + | completion | 0 | + And I am on the "Course 1" course page logged in as teacher1 + When I navigate to "Course completion" in current page administration + And I select "Default activity completion" from the "Course completion tertiary navigation" singleselect + And I click on "Glossaries" "checkbox" + And I click on "Edit" "button" + And I set the following fields to these values: + | completion | Show activity as complete when conditions are met | + | completionview | 0 | + | completionusegrade | 1 | + | completionentriesenabled | 1 | + | completionentries | 2 | + And I click on "Save changes" "button" + Then I should see "Changes saved" + And I am on "Course 1" course homepage with editing mode on + And I press "Add an activity or resource" + And I click on "Add a new Glossary" "link" in the "Add an activity or resource" "dialogue" + And I expand all fieldsets + # Completion tracking 2 = Show activity as complete when conditions are met. + And the field "Completion tracking" matches value "2" + And the field "completionview" matches value "0" + And the field "completionusegrade" matches value "1" + And the field "completionentriesenabled" matches value "1" + And the field "completionentries" matches value "2" + But I am on the "Test glossary one" Activity page + And I navigate to "Settings" in current page administration + And I expand all fieldsets + # Completion tracking 0 = Do not indicate activity completion. + And the field "Completion tracking" matches value "0" + + Scenario: Edit default activity completion without rules for automatic completion + Given I am on the "Course 1" course page logged in as teacher1 + When I navigate to "Course completion" in current page administration + And I select "Default activity completion" from the "Course completion tertiary navigation" singleselect + And I click on "Assignments" "checkbox" + And I click on "Edit" "button" + And I set the following fields to these values: + | completion | Show activity as complete when conditions are met | + | completionview | 0 | + | completionusegrade | 0 | + | completionsubmit | 0 | + And I click on "Save changes" "button" + Then I should see "When you select automatic completion, you must also enable at least one requirement (below)." + And I should not see "Changes saved" diff --git a/completion/upgrade.txt b/completion/upgrade.txt index 52a90662f6d..f025df10f37 100644 --- a/completion/upgrade.txt +++ b/completion/upgrade.txt @@ -1,6 +1,10 @@ -This files describes API changes in /completion/* - completion, +This file describes API changes in /completion/* - completion, information provided here is intended especially for developers. +=== 4.3 === +* A trait class, core_completion/form/form_trait has been added to reuse code for adding and validation completion settings to any + form. + === 4.0 === * New method mark_course_completions_activity_criteria() has been added to mark course completions instantly. It is based on cron for completion_criteria_activity.php which is refactored to use it as well. diff --git a/course/moodleform_mod.php b/course/moodleform_mod.php index b48e583445f..56b8c87249e 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -14,20 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Moodleform. - * - * @package core_course - * @copyright Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir.'/formslib.php'); -require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/plagiarismlib.php'); -use core\content\export\exporters\component_exporter; use core_grades\component_gradeitems; /** @@ -36,8 +28,12 @@ use core_grades\component_gradeitems; * * @package core_course * @copyright Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class moodleform_mod extends moodleform { + + use \core_completion\form\form_trait; + /** Current data */ protected $current; /** @@ -321,71 +317,8 @@ abstract class moodleform_mod extends moodleform { } } - // Completion: If necessary, freeze fields - $completion = new completion_info($COURSE); - if ($completion->is_enabled()) { - // If anybody has completed the activity, these options will be 'locked' - $completedcount = empty($this->_cm) - ? 0 - : $completion->count_user_data($this->_cm); - - $freeze = false; - if (!$completedcount) { - if ($mform->elementExists('unlockcompletion')) { - $mform->removeElement('unlockcompletion'); - } - // Automatically set to unlocked (note: this is necessary - // in order to make it recalculate completion once the option - // is changed, maybe someone has completed it now) - $mform->getElement('completionunlocked')->setValue(1); - } else { - // Has the element been unlocked, either by the button being pressed - // in this request, or the field already being set from a previous one? - if ($mform->exportValue('unlockcompletion') || - $mform->exportValue('completionunlocked')) { - // Yes, add in warning text and set the hidden variable - $mform->insertElementBefore( - $mform->createElement('static', 'completedunlocked', - get_string('completedunlocked', 'completion'), - get_string('completedunlockedtext', 'completion')), - 'unlockcompletion'); - $mform->removeElement('unlockcompletion'); - $mform->getElement('completionunlocked')->setValue(1); - } else { - // No, add in the warning text with the count (now we know - // it) before the unlock button - $mform->insertElementBefore( - $mform->createElement('static', 'completedwarning', - get_string('completedwarning', 'completion'), - get_string('completedwarningtext', 'completion', $completedcount)), - 'unlockcompletion'); - $freeze = true; - } - } - - if ($freeze) { - $mform->freeze('completion'); - if ($mform->elementExists('completionview')) { - $mform->freeze('completionview'); // don't use hardFreeze or checkbox value gets lost - } - if ($mform->elementExists('completionusegrade')) { - $mform->freeze('completionusegrade'); - } - if ($mform->elementExists('completionpassgrade')) { - $mform->freeze('completionpassgrade'); - - // Has the completion pass grade completion criteria been set? - // If it has then we shouldn't change the gradepass field. - if ($mform->exportValue('completionpassgrade')) { - $mform->freeze('gradepass'); - } - } - if ($mform->elementExists('completiongradeitemnumber')) { - $mform->freeze('completiongradeitemnumber'); - } - $mform->freeze($this->_customcompletionelements); - } - } + // Completion: If necessary, freeze fields. + $this->definition_after_data_completion(); // Freeze admin defaults if required (and not different from default) $this->apply_admin_locked_flags(); @@ -509,33 +442,8 @@ abstract class moodleform_mod extends moodleform { } } - // Completion: Don't let them choose automatic completion without turning - // on some conditions. Ignore this check when completion settings are - // locked, as the options are then disabled. - $automaticcompletion = array_key_exists('completion', $data); - $automaticcompletion = $automaticcompletion && $data['completion'] == COMPLETION_TRACKING_AUTOMATIC; - $automaticcompletion = $automaticcompletion && !empty($data['completionunlocked']); - - if ($automaticcompletion) { - // View to complete. - $rulesenabled = !empty($data['completionview']); - - // Use grade to complete (only one grade item). - $rulesenabled = $rulesenabled || !empty($data['completionusegrade']) || !empty($data['completionpassgrade']); - - // Use grade to complete (specific grade item). - if (!$rulesenabled && isset($data['completiongradeitemnumber'])) { - $rulesenabled = $data['completiongradeitemnumber'] != ''; - } - - // Module-specific completion rules. - $rulesenabled = $rulesenabled || $this->completion_rule_enabled($data); - - if (!$rulesenabled) { - // No rules are enabled. Can't set automatically completed without rules. - $errors['completion'] = get_string('badautocompletion', 'completion'); - } - } + // Completion: Check completion fields don't have errors. + $errors = array_merge($errors, $this->validate_completion($data)); // Availability: Check availability field does not have errors. if (!empty($CFG->enableavailability)) { @@ -730,139 +638,11 @@ abstract class moodleform_mod extends moodleform { if(!isset($completion)) { $completion = new completion_info($COURSE); } + + // Add the completion tracking elements to the form. if ($completion->is_enabled()) { $mform->addElement('header', 'activitycompletionheader', get_string('activitycompletion', 'completion')); - // Unlock button for if people have completed it (will - // be removed in definition_after_data if they haven't) - $mform->addElement('submit', 'unlockcompletion', get_string('unlockcompletion', 'completion')); - $mform->registerNoSubmitButton('unlockcompletion'); - $mform->addElement('hidden', 'completionunlocked', 0); - $mform->setType('completionunlocked', PARAM_INT); - - $trackingdefault = COMPLETION_TRACKING_NONE; - // If system and activity default is on, set it. - if ($CFG->completiondefault && $this->_features->defaultcompletion) { - $hasrules = plugin_supports('mod', $this->_modname, FEATURE_COMPLETION_HAS_RULES, true); - $tracksviews = plugin_supports('mod', $this->_modname, FEATURE_COMPLETION_TRACKS_VIEWS, true); - if ($hasrules || $tracksviews) { - $trackingdefault = COMPLETION_TRACKING_AUTOMATIC; - } else { - $trackingdefault = COMPLETION_TRACKING_MANUAL; - } - } - - $mform->addElement('select', 'completion', get_string('completion', 'completion'), - array(COMPLETION_TRACKING_NONE=>get_string('completion_none', 'completion'), - COMPLETION_TRACKING_MANUAL=>get_string('completion_manual', 'completion'))); - $mform->setDefault('completion', $trackingdefault); - $mform->addHelpButton('completion', 'completion', 'completion'); - - // Automatic completion once you view it - $gotcompletionoptions = false; - if (plugin_supports('mod', $this->_modname, FEATURE_COMPLETION_TRACKS_VIEWS, false)) { - $mform->addElement('checkbox', 'completionview', get_string('completionview', 'completion'), - get_string('completionview_desc', 'completion')); - $mform->hideIf('completionview', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); - // Check by default if automatic completion tracking is set. - if ($trackingdefault == COMPLETION_TRACKING_AUTOMATIC) { - $mform->setDefault('completionview', 1); - } - $gotcompletionoptions = true; - } - - if (plugin_supports('mod', $this->_modname, FEATURE_GRADE_HAS_GRADE, false)) { - // This activity supports grading. - $gotcompletionoptions = true; - - $component = "mod_{$this->_modname}"; - $itemnames = component_gradeitems::get_itemname_mapping_for_component($component); - - if (count($itemnames) === 1) { - // Only one gradeitem in this activity. - // We use the completionusegrade field here. - $mform->addElement( - 'checkbox', - 'completionusegrade', - get_string('completionusegrade', 'completion'), - get_string('completionusegrade_desc', 'completion') - ); - $mform->hideIf('completionusegrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); - $mform->addHelpButton('completionusegrade', 'completionusegrade', 'completion'); - - // Complete if the user has reached the pass grade. - $mform->addElement( - 'checkbox', - 'completionpassgrade', null, - get_string('completionpassgrade_desc', 'completion') - ); - $mform->hideIf('completionpassgrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); - $mform->disabledIf('completionpassgrade', 'completionusegrade', 'notchecked'); - $mform->addHelpButton('completionpassgrade', 'completionpassgrade', 'completion'); - - // The disabledIf logic differs between ratings and other grade items due to different field types. - if ($this->_features->rating) { - // If using the rating system, there is no grade unless ratings are enabled. - $mform->disabledIf('completionusegrade', 'assessed', 'eq', 0); - $mform->disabledIf('completionpassgrade', 'assessed', 'eq', 0); - } else { - // All other field types use the '$gradefieldname' field's modgrade_type. - $itemnumbers = array_keys($itemnames); - $itemnumber = array_shift($itemnumbers); - $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade'); - $mform->disabledIf('completionusegrade', "{$gradefieldname}[modgrade_type]", 'eq', 'none'); - $mform->disabledIf('completionpassgrade', "{$gradefieldname}[modgrade_type]", 'eq', 'none'); - } - } else if (count($itemnames) > 1) { - // There are multiple grade items in this activity. - // Show them all. - $options = [ - '' => get_string('activitygradenotrequired', 'completion'), - ]; - foreach ($itemnames as $itemnumber => $itemname) { - $options[$itemnumber] = get_string("grade_{$itemname}_name", $component); - } - - $mform->addElement( - 'select', - 'completiongradeitemnumber', - get_string('completionusegrade', 'completion'), - $options - ); - $mform->hideIf('completiongradeitemnumber', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); - - // Complete if the user has reached the pass grade. - $mform->addElement( - 'checkbox', - 'completionpassgrade', null, - get_string('completionpassgrade_desc', 'completion') - ); - $mform->hideIf('completionpassgrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); - $mform->disabledIf('completionpassgrade', 'completiongradeitemnumber', 'eq', ''); - $mform->addHelpButton('completionpassgrade', 'completionpassgrade', 'completion'); - } - } - - // Automatic completion according to module-specific rules - $this->_customcompletionelements = $this->add_completion_rules(); - foreach ($this->_customcompletionelements as $element) { - $mform->hideIf($element, 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); - } - - $gotcompletionoptions = $gotcompletionoptions || - count($this->_customcompletionelements)>0; - - // Automatic option only appears if possible - if ($gotcompletionoptions) { - $mform->getElement('completion')->addOption( - get_string('completion_automatic', 'completion'), - COMPLETION_TRACKING_AUTOMATIC); - } - - // Completion expected at particular date? (For progress tracking) - $mform->addElement('date_time_selector', 'completionexpected', get_string('completionexpected', 'completion'), - array('optional' => true)); - $mform->addHelpButton('completionexpected', 'completionexpected', 'completion'); - $mform->hideIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE); + $this->add_completion_elements(); } // Populate module tags.