Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Activity completion API: Difference between revisions

From MoodleDocs
m (→‎Example: Fix usage of $DB->get_record() in the example)
(25 intermediate revisions by 12 users not shown)
Line 39: Line 39:
In your module's ''modulename''<tt>_supports</tt> function, return true for FEATURE_COMPLETION_TRACKS_VIEWS.
In your module's ''modulename''<tt>_supports</tt> function, return true for FEATURE_COMPLETION_TRACKS_VIEWS.


Then add this code to run whenever a user successfully views the activity (for example, near the <tt>print_footer()</tt> call in <tt>view.php</tt>):
Then add this code to run whenever a user successfully views the activity. In order for navigation to work as expected (i.e. so that the navigation block on the module's page takes account that you have viewed this activity, if there is another activity that depends on it) you should put this code before printing the page header.


  $completion=new completion_info($course);
  $completion = new completion_info($course);
  $completion->set_module_viewed($cm);
  $completion->set_module_viewed($cm);


Line 72: Line 72:
# Return true for FEATURE_COMPLETION_HAS_RULES in your module's _supports function.
# Return true for FEATURE_COMPLETION_HAS_RULES in your module's _supports function.
# Add database fields to your module's main table to store the custom completion settings.
# Add database fields to your module's main table to store the custom completion settings.
# Add backup and restore code to back up these fields.
# Add controls to your module's settings form so that users can select the custom rules, altering these database settings.
# Add controls to your module's settings form so that users can select the custom rules, altering these database settings.
# Add a function that checks the value of these rules (if set).
# Add a function that checks the value of these rules (if set).
Line 86: Line 87:
* The main table is used for most other module options so it is a logical place for this information.
* The main table is used for most other module options so it is a logical place for this information.


If you are adding a basic completion condition you probably only need to add one field. To add a field to an existing module, you need to change the install.xml and then the update.php in the same way as adding any other field.
If you are adding a basic completion condition you probably only need to add one field. To add a field to an existing module, you need to change the db/install.xml and the db/upgrade.php in the same way as adding any other field.


==== Example ====
==== Example ====
Line 95: Line 96:


* '''completionposts''' - this may be 0 or an integer. If it's an integer, say 3, then the user needs to add 3 forum posts (either new discussions or replies) in order for the forum to count as 'completed'.
* '''completionposts''' - this may be 0 or an integer. If it's an integer, say 3, then the user needs to add 3 forum posts (either new discussions or replies) in order for the forum to count as 'completed'.
=== Backup and restore for completion fields ===
Modules do not need to back up the generic completion options, which are handled by the system, but they do need to back up any custom options. You should add backup and restore logic for the fields mentioned above.
Remember that your restore code should handle the case when these fields are not present, setting the fields to a suitable default value.
==== Example ====
The following code in <tt>backup_forum_stepslib.php</tt> lists the fields to back up:
  $forum = new backup_nested_element('forum', array('id'), array(
            'type', 'name', 'intro', 'introformat',
            'assessed', 'assesstimestart', 'assesstimefinish', 'scale',
            'maxbytes', 'maxattachments', 'forcesubscribe', 'trackingtype',
            'rsstype', 'rssarticles', 'timemodified', 'warnafter',
            'blockafter', 'blockperiod', 'completiondiscussions', 'completionreplies',
            'completionposts'));
As you can see, I added the '''completionposts''' field (and the others that aren't covered in this example) to the list of fields.


=== Form changes for completion settings ===
=== Form changes for completion settings ===
Line 112: Line 133:
First, the function that adds these controls:
First, the function that adds these controls:


  function add_completion_rules() {
<code>
    $mform =& $this->_form;
/**
* Add elements for setting the custom completion rules.
    $group=array();
 
    $group[] =& $mform->createElement('checkbox', 'completionpostsenabled', '', get_string('completionposts','forum'));
* @category completion
    $group[] =& $mform->createElement('text', 'completionposts', '', array('size'=>3));
* @return array List of added element names, or names of wrapping group elements.
    $mform->setType('completionposts',PARAM_INT);
*/
    $mform->addGroup($group, 'completionpostsgroup', get_string('completionpostsgroup','forum'), array(' '), false);
public function add_completion_rules() {
    $mform->setHelpButton('completionpostsgroup', array('completion', get_string('completionpostshelp', 'forum'), 'forum'));
 
    $mform->disabledIf('completionposts','completionpostsenabled','notchecked');
    $mform = $this->_form;
 
    return array('completionpostsgroup');
    $group = [
}
        $mform->createElement('checkbox', 'completionpostsenabled', ' ', get_string('completionposts', 'forum')),
        $mform->createElement('text', 'completionposts', ' ', ['size' => 3]),
    ];
    $mform->setType('completionposts', PARAM_INT);
    $mform->addGroup($group, 'completionpostsgroup', get_string('completionpostsgroup','forum'), [' '], false);
    $mform->addHelpButton('completionpostsgroup', 'completionposts', 'forum');
    $mform->disabledIf('completionposts', 'completionpostsenabled', 'notchecked');
 
    return ['completionpostsgroup'];
}
</code>


* The function creates a checkbox and a text input field, which is set to accept only numbers.
* The function creates a checkbox and a text input field, which is set to accept only numbers.
Line 134: Line 165:
Next, a function for checking whether the user selected this option:
Next, a function for checking whether the user selected this option:


  function completion_rule_enabled($data) {
<code>
    return (!empty($data['completionpostsenabled']) && $data['completionposts']!=0);
/**
}
  * Called during validation to see whether some module-specific completion rules are 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['completionpostsenabled']) && $data['completionposts'] != 0);
}
</code>


* The custom completion rule is enabled if the 'enabled' checkbox is ticked and the text field value is something other than zero.
* The custom completion rule is enabled if the 'enabled' checkbox is ticked and the text field value is something other than zero.
** This is used to give an error if the user selects automatic completion, but fails to select any conditions.
** This is used to give an error if the user selects automatic completion, but fails to select any conditions.


That's all the 'required' functions, but we need to add some extra code to support the checkbox behaviour. I overrode get_data so that if there is a value in the edit field, but the checkbox is not ticked, the value counts as zero (the rule will not be enabled):
That's all the 'required' functions, but we need to add some extra code to support the checkbox behaviour. I overrode get_data so that if there is a value in the edit field, but the checkbox is not ticked, the value counts as zero (the rule will not be enabled).


  function get_data() {
  function get_data() {
     $data=parent::get_data();
     $data = parent::get_data();
     if(!$data) {
     if (!$data) {
         return false;
         return $data;
     }
     }
     // Turn off completion settings if the checkboxes aren't ticked
     if (!empty($data->completionunlocked)) {
    $autocompletion=!empty($data->completion) && $data->completion==COMPLETION_TRACKING_AUTOMATIC;
        // Turn off completion settings if the checkboxes aren't ticked
    if(empty($data->completionpostsenabled) || !$autocompletion) {
        $autocompletion = !empty($data->completion) && $data->completion==COMPLETION_TRACKING_AUTOMATIC;
        $data->completionposts=0;
        if (empty($data->completionpostsenabled) || !$autocompletion) {
            $data->completionposts = 0;
        }
     }
     }
     return $data;
     return $data;
  }
  }
You may have noticed the 'completionunlocked' check. When some users have already completed the activity, the completion settings are 'locked'; they are disabled and cannot be edited, so there will be no value set for those fields in the <code>$data</code> object. Normally this will automatically work but when dealing with checkboxes you need to include a check for the 'completionunlocked' value before doing anything that would cause one of those fields to be changed in the database.


Finally, forum already had a <tt>data_preprocessing</tt> function but I added code to this to set up the checkboxes when the form is displayed, and to make the default value of the text fields 1 instead of 0:
Finally, forum already had a <tt>data_preprocessing</tt> function but I added code to this to set up the checkboxes when the form is displayed, and to make the default value of the text fields 1 instead of 0:
Line 192: Line 236:
Here's the function for forum (simplified to include only the one completion option):
Here's the function for forum (simplified to include only the one completion option):


<code>
  /**
  /**
   * Obtains the automatic completion state for this forum based on any conditions
   * Obtains the automatic completion state for this forum based on any conditions
Line 206: Line 251:
   
   
     // Get forum details
     // Get forum details
     if(!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
     $forum = $DB->get_record('forum', array('id' => $cm->instance), '*', MUST_EXIST);
        throw new Exception("Can't find forum {$cm->instance}");
    }
   
   
     // If completion option is enabled, evaluate it and return true/false  
     // If completion option is enabled, evaluate it and return true/false  
Line 226: Line 269:
     }
     }
  }
  }
</code>


=== Notifying the completion system ===
=== Notifying the completion system ===
Line 236: Line 280:
** '''COMPLETION_INCOMPLETE''' - this change will either have no effect on the user's completion state, or it will make it incomplete. The change cannot make a user's state complete if it was incomplete previously. Deleting a forum post would fall into this category.
** '''COMPLETION_INCOMPLETE''' - this change will either have no effect on the user's completion state, or it will make it incomplete. The change cannot make a user's state complete if it was incomplete previously. Deleting a forum post would fall into this category.
** '''COMPLETION_UNKNOWN''' - this change might have either effect. Using this option is much slower than the others, so try to avoid using it in anything that might happen frequently.
** '''COMPLETION_UNKNOWN''' - this change might have either effect. Using this option is much slower than the others, so try to avoid using it in anything that might happen frequently.
* If the user whose completion state would be updated is not the current user, then the optional <tt>$userid</tt> parameter must be included. For example, if a teacher deletes a student's forum post, then it is the student's completion state which may need updating, not the teacher's.


==== Example ====
==== Example ====
Line 243: Line 288:
  // Update completion state
  // Update completion state
  $completion=new completion_info($course);
  $completion=new completion_info($course);
  if($completion->is_enabled($cm) && $forum->completionposts)) {
  if($completion->is_enabled($cm) && $forum->completionposts) {
     $completion->update_state($cm,COMPLETION_COMPLETE);
     $completion->update_state($cm,COMPLETION_COMPLETE);
  }
  }
=== Completion Checks in Cron Tasks ===
If you need to check completion as part of a cron task or another part of Moodle that does not already include the completion_info class, you will need to include it.
==== Example ====
require_once($CFG->dirroot.'/lib/completionlib.php');
== See Also ==
[[Conditional activities|Activity completion and availability]] - Original Specification
[[Course completion]] - Original Specification
[[Policy - Retroactive effects of completion settings]]
[[Core APIs]]
=== User Docs ===
[https://docs.moodle.org/en/Category:Completion Completion Docs]
[https://docs.moodle.org/en/Activity_completion Activity Completion]
[https://docs.moodle.org/en/Course_completion Course Completion]
[[Category:Conditional activities]]
[[Category:API]]

Revision as of 22:00, 13 July 2017

Moodle 2.0


Modules do not need to be changed to support conditional availability, but they do need changing to support the completion system.

If you make no changes to a module whatsoever, it can only support 'manual' completion (where the user ticks a box).

Feature support

To support the completion system, your module must include a modulename_supports function in its lib.php. Here is an example:

/**
 * Indicates API features that the forum supports.
 *
 * @param string $feature
 * @return mixed True if yes (some features may use other values)
 */
function forum_supports($feature) {
    switch($feature) {
        case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
        case FEATURE_COMPLETION_HAS_RULES: return true;
        default: return null;
    }
}

The relevant features for completion are:

  • FEATURE_COMPLETION_TRACKS_VIEWS - the module can support completion 'on view', meaning that an activity becomes marked complete as soon as a user clicks on it.
  • FEATURE_GRADE_HAS_GRADE - the module provides (or may provide, depending on settings) a grade for students. When a module supports grades, it can support completion 'on grade', meaning that an activity becomes marked complete as soon as a user is assigned a grade.
  • FEATURE_COMPLETION_HAS_RULES - the module has custom completion rules.

Completion on view

Completion on view means that, if selected, an activity is marked complete as soon as the user views it. 'View' is usually defined as seeing the module's main page; if you click on the activity, and there isn't an error, you have probably viewed it. However it is up to each module precisely how they define 'view'.

How to implement

In your module's modulename_supports function, return true for FEATURE_COMPLETION_TRACKS_VIEWS.

Then add this code to run whenever a user successfully views the activity. In order for navigation to work as expected (i.e. so that the navigation block on the module's page takes account that you have viewed this activity, if there is another activity that depends on it) you should put this code before printing the page header.

$completion = new completion_info($course);
$completion->set_module_viewed($cm);

Performance issues

Calling this method has no significant performance cost if 'on view' completion is not enabled for the activity. If it is enabled, then the performance cost is kept low because the 'viewed' state is cached; it doesn't add a database query to every request.

Completion on grade

Completion on grade means that, if selected, an activity is marked complete as soon as the user receives a grade from that activity.

How to implement

In your module's _supports function, return true for FEATURE_GRADE_HAS_GRADE. No other action is necessary.

Performance issues

When 'on grade' completion is enabled, there will be some additional database queries after a grade is assigned or changed. Unless your activity changes grades very frequently, this is unlikely to be an issue.

Custom completion rules

Custom completion rules allow for module-specific conditions. For example, the forum has custom rules so that you can make it mark a user completed when they make a certain number of posts to the forum.

It is a lot harder to implement custom completion rules than it is to use the system-provided 'view' or 'grade' conditions, but the instructions below should help make it clear.

Implementation overview

To implement custom completion rules, you need to:

  1. Return true for FEATURE_COMPLETION_HAS_RULES in your module's _supports function.
  2. Add database fields to your module's main table to store the custom completion settings.
  3. Add backup and restore code to back up these fields.
  4. Add controls to your module's settings form so that users can select the custom rules, altering these database settings.
  5. Add a function that checks the value of these rules (if set).
  6. Add code so that whenever the value affecting a rule might change, you inform the completion system.

Database fields for completion settings

When you provide a custom completion rule for a module, that rule requires data to be stored with each module instance: whether the rule is enabled for that instance, and any options that apply to the rule.

Usually the best place to store this information is your module's main table because:

  • The information in the relevant row of this table is likely to be available in most parts of your code, so code changes are minimised.
  • You already read this row with most requests, so there is no need for additional database queries which would reduce performance.
  • The main table is used for most other module options so it is a logical place for this information.

If you are adding a basic completion condition you probably only need to add one field. To add a field to an existing module, you need to change the db/install.xml and the db/upgrade.php in the same way as adding any other field.

Example

Throughout this section I am using the forum as an example. The forum provides three completion options but because they all behave the same way, I am only showing one of them.

The forum adds this field to store a completion option:

  • completionposts - this may be 0 or an integer. If it's an integer, say 3, then the user needs to add 3 forum posts (either new discussions or replies) in order for the forum to count as 'completed'.

Backup and restore for completion fields

Modules do not need to back up the generic completion options, which are handled by the system, but they do need to back up any custom options. You should add backup and restore logic for the fields mentioned above.

Remember that your restore code should handle the case when these fields are not present, setting the fields to a suitable default value.

Example

The following code in backup_forum_stepslib.php lists the fields to back up:

 $forum = new backup_nested_element('forum', array('id'), array(
           'type', 'name', 'intro', 'introformat',
           'assessed', 'assesstimestart', 'assesstimefinish', 'scale',
           'maxbytes', 'maxattachments', 'forcesubscribe', 'trackingtype',
           'rsstype', 'rssarticles', 'timemodified', 'warnafter',
           'blockafter', 'blockperiod', 'completiondiscussions', 'completionreplies',
           'completionposts'));

As you can see, I added the completionposts field (and the others that aren't covered in this example) to the list of fields.

Form changes for completion settings

When you have custom completion conditions, you need to add controls to your module's settings form mod_form.php so that users can select these conditions. You can add any necessary controls.

  • Implement the add_completion_rules function which adds the form controls for your new rules.
  • Implement the completion_rule_enabled function which is called during form validation to check whether one of your module's completion rules has been selected.
  • Implement other form changes if necessary to set up the form with your data. If your data is in the form of simple text boxes or dropdowns then this is not necessary, but you might want to have a checkbox that enables the rule with a separate control to set its value. This needs form tweaks.

Example

The forum offers a checkbox with a text input box beside it. You tick the checkbox to enable the rule, then type in the desired number of posts.

First, the function that adds these controls:

/**

* Add elements for setting the custom completion rules.
*  
* @category completion
* @return array List of added element names, or names of wrapping group elements.
*/

public function add_completion_rules() {

   $mform = $this->_form;
   $group = [
       $mform->createElement('checkbox', 'completionpostsenabled', ' ', get_string('completionposts', 'forum')),
       $mform->createElement('text', 'completionposts', ' ', ['size' => 3]),
   ];
   $mform->setType('completionposts', PARAM_INT);
   $mform->addGroup($group, 'completionpostsgroup', get_string('completionpostsgroup','forum'), [' '], false);
   $mform->addHelpButton('completionpostsgroup', 'completionposts', 'forum');
   $mform->disabledIf('completionposts', 'completionpostsenabled', 'notchecked');
   return ['completionpostsgroup'];

}

  • The function creates a checkbox and a text input field, which is set to accept only numbers.
  • These are grouped together so they appear on the same line, and we add a help button.
  • The text input field is disabled if the checkbox isn't ticked.
  • Note that this function must return the top-level element associated with the completion rule. (If there are multiple elements, you can return more than one.)
    • This is used so that your controls become disabled if automatic completion is not selected.

Next, a function for checking whether the user selected this option:

/**

* Called during validation to see whether some module-specific completion rules are 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['completionpostsenabled']) && $data['completionposts'] != 0);

}

  • The custom completion rule is enabled if the 'enabled' checkbox is ticked and the text field value is something other than zero.
    • This is used to give an error if the user selects automatic completion, but fails to select any conditions.

That's all the 'required' functions, but we need to add some extra code to support the checkbox behaviour. I overrode get_data so that if there is a value in the edit field, but the checkbox is not ticked, the value counts as zero (the rule will not be enabled).

function get_data() {
    $data = parent::get_data();
    if (!$data) {
        return $data;
    }
    if (!empty($data->completionunlocked)) {
        // Turn off completion settings if the checkboxes aren't ticked
        $autocompletion = !empty($data->completion) && $data->completion==COMPLETION_TRACKING_AUTOMATIC;
        if (empty($data->completionpostsenabled) || !$autocompletion) {
           $data->completionposts = 0;
        }
    }
    return $data;
}


You may have noticed the 'completionunlocked' check. When some users have already completed the activity, the completion settings are 'locked'; they are disabled and cannot be edited, so there will be no value set for those fields in the $data object. Normally this will automatically work but when dealing with checkboxes you need to include a check for the 'completionunlocked' value before doing anything that would cause one of those fields to be changed in the database.

Finally, forum already had a data_preprocessing function but I added code to this to set up the checkboxes when the form is displayed, and to make the default value of the text fields 1 instead of 0:

function data_preprocessing(&$default_values){
    // [Existing code, not shown]

    // Set up the completion checkboxes which aren't part of standard data.
    // We also make the default value (if you turn on the checkbox) for those
    // numbers to be 1, this will not apply unless checkbox is ticked.
    $default_values['completionpostsenabled']=
        !empty($default_values['completionposts']) ? 1 : 0;
    if(empty($default_values['completionposts'])) {
        $default_values['completionposts']=1;
    }
}

Phew! That's the form done.

Completion state function

When you create completion conditions, you need to write a function module_get_completion_state that checks the value of those conditions for a particular user.

The function receives as parameters $course, $cm, and $userid - all self-explanatory, I hope - and $type. This has two values:

  • COMPLETION_AND - if multiple conditions are selected, the user must meet all of them.
  • COMPLETION_OR (not currently used) - if multiple conditions are selected, any one of them is good enough to complete the activity.

Your function should return:

  • true if your custom completion options are enabled and the user meets the conditions.
  • false if your custom completion options are enabled but the user does not yet meet the conditions.
  • $type (not false!) if none of your custom completion options are not enabled.

Example

Here's the function for forum (simplified to include only the one completion option):

/**
 * Obtains the automatic completion state for this forum based on any conditions
 * in forum settings.
 *
 * @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, $type if conditions not set.
 */
function forum_get_completion_state($course,$cm,$userid,$type) {
    global $CFG,$DB;

    // Get forum details
    $forum = $DB->get_record('forum', array('id' => $cm->instance), '*', MUST_EXIST);

    // If completion option is enabled, evaluate it and return true/false 
    if($forum->completionposts) {
        return $forum->completionposts <= $DB->get_field_sql("
SELECT 
    COUNT(1) 
FROM 
    {forum_posts} fp 
    INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
WHERE
    fp.userid=:userid AND fd.forum=:forumid",
            array('userid'=>$userid,'forumid'=>$forum->id));
    } else {
        // Completion option is not enabled so just return $type
        return $type;
    }
}

Notifying the completion system

Finally you need to notify the completion system whenever these values might have changed for a user (in the case of the forum example, whenever somebody adds or deletes a post). The completion system will end up calling the function above - but only if it needs to.

  • To ensure performance is not compromised, you should notify the system only when the completion state might actually have changed. Don't notify the system unless your custom completion rule is actually enabled.
  • You need to pass in the 'possible result' of the change. This is used to significantly improve performance. There are three values:
    • COMPLETION_COMPLETE - this change will either have no effect on the user's completion state, or it will make it complete. The change cannot make a user's state incomplete if it was complete previously. In the forum example, when you add a post, there is no way this can make the user's state incomplete, so this possible result applies.
    • COMPLETION_INCOMPLETE - this change will either have no effect on the user's completion state, or it will make it incomplete. The change cannot make a user's state complete if it was incomplete previously. Deleting a forum post would fall into this category.
    • COMPLETION_UNKNOWN - this change might have either effect. Using this option is much slower than the others, so try to avoid using it in anything that might happen frequently.
  • If the user whose completion state would be updated is not the current user, then the optional $userid parameter must be included. For example, if a teacher deletes a student's forum post, then it is the student's completion state which may need updating, not the teacher's.

Example

Here's the code that runs when somebody makes a new forum post:

// Update completion state
$completion=new completion_info($course);
if($completion->is_enabled($cm) && $forum->completionposts) {
    $completion->update_state($cm,COMPLETION_COMPLETE);
}

Completion Checks in Cron Tasks

If you need to check completion as part of a cron task or another part of Moodle that does not already include the completion_info class, you will need to include it.

Example

require_once($CFG->dirroot.'/lib/completionlib.php');

See Also

Activity completion and availability - Original Specification

Course completion - Original Specification

Policy - Retroactive effects of completion settings

Core APIs

User Docs

Completion Docs

Activity Completion

Course Completion